Open7

UniswapXのdirect fillを理解する

ywzxywzx

目的

  • UniswapXのdirectFillの動作について、コードレベルで理解する
    • 特に、UniswapXを介して提出されたintentの署名はどのようにオンチェーンで使われるのか。その署名データがfront-runningされることはないのか。
  • dutch auctionはオフチェーンで行われてると思うが、その仕様もあればみたい

結果

特に、UniswapXを介して提出されたintentの署名はどのようにオンチェーンで使われるのか。その署名データがfront-runningされることはないのか。

direct fillでswapするトークンの送り先(filler)とswapされたトークンの送り先(swapper)が、directFill関数の引数のsignedOrder.orderから生成されており、signedOrder.orderの生成のされ方ではfront-runされる気がした。

攻撃の手法として、「悪意のあるFillerがintentを横取りして自身のFillにする=Fillerのアドレスの入れ替え」「tokenを受け取るだけして送信しない = Swapperのアドレスの入れ替え(これはfront-runningより単純な攻撃)」2パターンある。

  • Fillerのアドレスの入れ替え

  • Swapperのアドレスの入れ替え

    • こちらも署名データにrecipientが登録されており、前処理で検証がされてるから入れ替えは不可能
    • 関数的に密結合になっているのでミスを犯しやすいが、gas最適化のためにはこの書き方になるよな。。。
    • DutchOrderに関しては要調査&まだdeployされてなさそう

dutch auctionはオフチェーンで行われてると思うが、その仕様もあればみたい

decay関数でオンチェーンに取り込まれた際の額をベースにamountを決定しているから、オフチェーンで約定しているわけではなさそう。ここは勘違いしていた。

ywzxywzx

UniswapXの起動方法

  • UniswapXを起動してUSED-Pepeなど流動性の低い取引をするとあわられる
ywzxywzx

Intentの署名

署名はPermit2で利用されているPermitWitnessTransferFromが利用されている。

実際に署名するデータの形はこれ

      "orderInfo": {
          "chainId": 1,
          "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
          "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
          "swapper": "0x0000000000000000000000000000000000000000",
          "nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
          "deadline": 1691176812,
          "additionalValidationContract": "0x0000000000000000000000000000000000000000",
          "additionalValidationData": "0x",
          "decayStartTime": 1691176740,
          "decayEndTime": 1691176800,
          "exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
          "exclusivityOverrideBps": "100",
          "input": {
              "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
              "startAmount": "1000000000000000000",
              "endAmount": "1000000000000000000"
          },
          "outputs": [
              {
                  "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
                  "startAmount": "929502510517534478575",
                  "endAmount": "919795986077127665276",
                  "recipient": "0x0000000000000000000000000000000000000000"
              }
          ]
      }

https://github.com/Uniswap/interface/blob/1ffaf723de84673351a78093dacb1d58c156ab87/cypress/fixtures/uniswapx/quote2.json#L4-L30

ywzxywzx

direct fillの実行

実行しているコントラクト
https://etherscan.io/address/0x6000da47483062a0d734ba3dc7576ce6a0b645c4

実行する関数:https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/reactors/BaseReactor.sol#L34C1-L41C6

    /// @inheritdoc IReactor
    function execute(SignedOrder calldata order) external payable override nonReentrant {
        ResolvedOrder[] memory resolvedOrders = new ResolvedOrder[](1);
        resolvedOrders[0] = resolve(order);

        _prepare(resolvedOrders);
        _fill(resolvedOrders);
    }

SignerdOrderの型:https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/base/ReactorStructs.sol#L54-L57

/// @dev external struct including a generic encoded order and swapper signature
///  The order bytes will be parsed and mapped to a ResolvedOrder in the concrete reactor contract
struct SignedOrder {
    bytes order;
    bytes sig;
}

_prepare()と_fill()が実行されている

ywzxywzx

prepare()

_prepare:https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/reactors/BaseReactor.sol#L94-L106

    /// @notice validates, injects fees, and transfers input tokens in preparation for order fill
    /// @param orders The orders to prepare
    function _prepare(ResolvedOrder[] memory orders) internal {
        uint256 ordersLength = orders.length;
        unchecked {
            for (uint256 i = 0; i < ordersLength; i++) {
                ResolvedOrder memory order = orders[i];
                _injectFees(order);
                order.validate(msg.sender);
                transferInputTokens(order, msg.sender);
            }
        }
    }

validate()

ResolvedOrderの型:https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/base/ReactorStructs.sol#L43-L50

/// @dev generic concrete order that specifies exact tokens which need to be sent and received
struct ResolvedOrder {
    OrderInfo info;
    InputToken input;
    OutputToken[] outputs;
    bytes sig;
    bytes32 hash;
}

/// @dev generic order information
///  should be included as the first field in any concrete order types
struct OrderInfo {
    // The address of the reactor that this order is targeting
    // Note that this must be included in every order so the swapper
    // signature commits to the specific reactor that they trust to fill their order properly
    IReactor reactor;
    // The address of the user which created the order
    // Note that this must be included so that order hashes are unique by swapper
    address swapper;
    // The nonce of the order, allowing for signature replay protection and cancellation
    uint256 nonce;
    // The timestamp after which this order is no longer valid
    uint256 deadline;
    // Custom validation contract
    IValidationCallback additionalValidationContract;
    // Encoded validation params for additionalValidationContract
    bytes additionalValidationData;
}

validation : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/lib/ResolvedOrderLib.sol#L16-L28

    function validate(ResolvedOrder memory resolvedOrder, address filler) internal view {
        if (address(this) != address(resolvedOrder.info.reactor)) {
            revert InvalidReactor();
        }

        if (block.timestamp > resolvedOrder.info.deadline) {
            revert DeadlinePassed();
        }

        if (address(resolvedOrder.info.additionalValidationContract) != address(0)) {
            resolvedOrder.info.additionalValidationContract.validate(filler, resolvedOrder);
        }
    }

transferInputTokens

transferInputTokens : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/reactors/DutchOrderReactor.sol#L47-L56

    /// @inheritdoc BaseReactor
    function transferInputTokens(ResolvedOrder memory order, address to) internal override {
        permit2.permitWitnessTransferFrom(
            order.toPermit(),
            order.transferDetails(to),
            order.info.swapper,
            order.hash,
            DutchOrderLib.PERMIT2_ORDER_TYPE,
            order.sig
        );
    }

permitWitnessTransferFrom : https://github.com/Uniswap/permit2/blob/main/src/SignatureTransfer.sol#L31-L43


    /// @inheritdoc ISignatureTransfer
    function permitWitnessTransferFrom(
        PermitTransferFrom memory permit,
        SignatureTransferDetails calldata transferDetails,
        address owner,
        bytes32 witness,
        string calldata witnessTypeString,
        bytes calldata signature
    ) external {
        _permitTransferFrom(
            permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
        );
    }

    /// @notice Transfers a token using a signed permit message.
    /// @param permit The permit data signed over by the owner
    /// @param dataHash The EIP-712 hash of permit data to include when checking signature
    /// @param owner The owner of the tokens to transfer
    /// @param transferDetails The spender's requested transfer details for the permitted token
    /// @param signature The signature to verify
    function _permitTransferFrom(
        PermitTransferFrom memory permit,
        SignatureTransferDetails calldata transferDetails,
        address owner,
        bytes32 dataHash,
        bytes calldata signature
    ) private {
        uint256 requestedAmount = transferDetails.requestedAmount;

        if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
        if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);

        _useUnorderedNonce(owner, permit.nonce);

        signature.verify(_hashTypedData(dataHash), owner);

        ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);
    }

この時点でswapperからのapprove+transferFromが行われる。
関数の処理を見ていると、transferInputTokens(order, msg.sender);で送り先を指定しているので、toを変更しようと思えば変更できる可能性あり。

ywzxywzx

resolve

その前に引数が整形されていたのでその関数

resolve : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/reactors/DutchOrderReactor.sol#L27-L44
署名はsignedOrder.sigがそのまま使われている
orderはsignedOrder.orderをdecodeしている。
つまり、signedOrder.orderにorderの情報が含まれており、signedOrder.sigはおそらくSwapperの署名

    function resolve(SignedOrder calldata signedOrder)
        internal
        view
        virtual
        override
        returns (ResolvedOrder memory resolvedOrder)
    {
        DutchOrder memory order = abi.decode(signedOrder.order, (DutchOrder));
        _validateOrder(order);

        resolvedOrder = ResolvedOrder({
            info: order.info,
            input: order.input.decay(order.decayStartTime, order.decayEndTime),
            outputs: order.outputs.decay(order.decayStartTime, order.decayEndTime),
            sig: signedOrder.sig,
            hash: order.hash()
        });
    }

DutchOrder : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/lib/DutchOrderLib.sol#L30-L41

struct DutchOrder {
    // generic order information
    OrderInfo info;
    // The time at which the DutchOutputs start decaying
    uint256 decayStartTime;
    // The time at which price becomes static
    uint256 decayEndTime;
    // The tokens that the swapper will provide when settling the order
    DutchInput input;
    // The tokens that must be received to satisfy the order
    DutchOutput[] outputs;
}

DutchOutput / DutchInput : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/lib/DutchOrderLib.sol#L8-L28

/// @dev An amount of output tokens that decreases linearly over time
struct DutchOutput {
    // The ERC20 token address (or native ETH address)
    address token;
    // The amount of tokens at the start of the time period
    uint256 startAmount;
    // The amount of tokens at the end of the time period
    uint256 endAmount;
    // The address who must receive the tokens to satisfy the order
    address recipient;
}

/// @dev An amount of input tokens that increases linearly over time
struct DutchInput {
    // The ERC20 token address
    ERC20 token;
    // The amount of tokens at the start of the time period
    uint256 startAmount;
    // The amount of tokens at the end of the time period
    uint256 endAmount;
}

decay : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/lib/DutchDecayLib.sol#L20-L48C6
ここでdutchauctionを実装している

    /// @notice calculates an amount using linear decay over time from decayStartTime to decayEndTime
    /// @dev handles both positive and negative decay depending on startAmount and endAmount
    /// @param startAmount The amount of tokens at decayStartTime
    /// @param endAmount The amount of tokens at decayEndTime
    /// @param decayStartTime The time to start decaying linearly
    /// @param decayEndTime The time to stop decaying linearly
    function decay(uint256 startAmount, uint256 endAmount, uint256 decayStartTime, uint256 decayEndTime)
        internal
        view
        returns (uint256 decayedAmount)
    {
        if (decayEndTime < decayStartTime) {
            revert EndTimeBeforeStartTime();
        } else if (decayEndTime <= block.timestamp) {
            decayedAmount = endAmount;
        } else if (decayStartTime >= block.timestamp) {
            decayedAmount = startAmount;
        } else {
            unchecked {
                uint256 elapsed = block.timestamp - decayStartTime;
                uint256 duration = decayEndTime - decayStartTime;
                if (endAmount < startAmount) {
                    decayedAmount = startAmount - (startAmount - endAmount).mulDivDown(elapsed, duration);
                } else {
                    decayedAmount = startAmount + (endAmount - startAmount).mulDivDown(elapsed, duration);
                }
            }
        }
    }
ywzxywzx

fill

_fill() : https://github.com/Uniswap/UniswapX/blob/4bc3d8d2f528223a0d6df5ac5348510883e9aa77/src/reactors/BaseReactor.sol#L108C1-L133C6

    /// @notice fills a list of orders, ensuring all outputs are satisfied
    /// @param orders The orders to fill
    function _fill(ResolvedOrder[] memory orders) internal {
        uint256 ordersLength = orders.length;
        // attempt to transfer all currencies to all recipients
        unchecked {
            // transfer output tokens to their respective recipients
            for (uint256 i = 0; i < ordersLength; i++) {
                ResolvedOrder memory resolvedOrder = orders[i];
                uint256 outputsLength = resolvedOrder.outputs.length;
                for (uint256 j = 0; j < outputsLength; j++) {
                    OutputToken memory output = resolvedOrder.outputs[j];
                    output.token.transferFill(output.recipient, output.amount);
                }

                emit Fill(orders[i].hash, msg.sender, resolvedOrder.info.swapper, resolvedOrder.info.nonce);
            }
        }

        // refund any remaining ETH to the filler. Only occurs when filler sends more ETH than required to
        // `execute()` or `executeBatch()`, or when there is excess contract balance remaining from others
        // incorrectly calling execute/executeBatch without direct filler method but with a msg.value
        if (address(this).balance > 0) {
            CurrencyLibrary.transferNative(msg.sender, address(this).balance);
        }
    }

output.recipientはsignedOrder.orderをdecodeしたものなので、fillerが任意に指定できそう。