UniversalUniswapV4Adapter

0xa687b664662b96b180346d699a6d5b42e9b05d31

Verification
Verified
v0.8.17+commit.8df45f5f
Type
Contract
5,773 bytes
ABI entries
12
2 read · 3 write
License
none

Contract information

Address
0xa687b664662b96b180346d699a6d5b42e9b05d31
Chain
Robinhood Chain (4663)
Compiler
v0.8.17+commit.8df45f5f
Optimization
Enabled
Creation tx
0x0c121bc932…1805bba5e8

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (2)

WETH()address
poolManager()address

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ERC20TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "NotEnoughLiquidity",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SafeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "moreInfo",
        "type": "bytes"
      }
    ],
    "name": "sellBase",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "moreInfo",
        "type": "bytes"
      }
    ],
    "name": "sellQuote",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IAdapter} from "@interfaces/IAdapter.sol";
import {IERC20} from "@interfaces/IERC20.sol";
import {
    BalanceDelta,
    BalanceDeltaLibrary,
    Currency,
    CurrencyLibrary,
    IHooks,
    IPoolManager,
    PoolId,
    PoolKey,
    PoolKeyLibrary,
    equals,
    lessThan
} from "@interfaces/IUniswapV4.sol";
import {IWETH} from "@interfaces/IWETH.sol";
import {SafeCallback} from "@libraries/SafeCallback.sol";
import {SafeERC20} from "@libraries/SafeERC20.sol";
import {TransientStateLibrary} from "@libraries/TransientStateLibrary.sol";

/// @title UniversalUniswapV4Adapter
/// @notice Adapter for all Uniswap V4 pools (vanilla or hooked: DX Terminal, Clanker, Zora, etc.).
/// @dev Uses the V4 singleton PoolManager via the unlock/settle/swap/take pattern.
///      Supports multi-hop swaps and native-ETH via WETH wrapping.
contract UniversalUniswapV4Adapter is IAdapter, SafeCallback {
    using SafeERC20 for IERC20;
    using CurrencyLibrary for Currency;
    using BalanceDeltaLibrary for BalanceDelta;
    using PoolKeyLibrary for PoolKey;
    using TransientStateLibrary for IPoolManager;

    address public immutable WETH;
    uint160 internal constant MIN_SQRT_PRICE = 4295128739;
    uint160 internal constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342;

    uint256 internal constant ADDRESS_MASK = 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff;

    error NotEnoughLiquidity(PoolId poolId);

    struct PathKey {
        Currency inputCurrency;
        Currency intermediateCurrency;
        uint24 fee;
        int24 tickSpacing;
        address hook;
        bytes hookData;
    }

    constructor(address poolManager_, address weth_) SafeCallback(IPoolManager(poolManager_)) {
        WETH = weth_;
    }

    // ──────────────────────── IAdapter ────────────────────────

    function sellBase(address to, address, bytes memory moreInfo) external override {
        uint256 payerOrigin;
        assembly {
            let size := calldatasize()
            payerOrigin := calldataload(sub(size, 32))
        }
        _unlock(to, payerOrigin, moreInfo);
    }

    function sellQuote(address to, address, bytes memory moreInfo) external override {
        uint256 payerOrigin;
        assembly {
            let size := calldatasize()
            payerOrigin := calldataload(sub(size, 32))
        }
        _unlock(to, payerOrigin, moreInfo);
    }

    // ──────────────────────── Unlock ────────────────────────

    function _unlock(address to, uint256 payerOrigin, bytes memory moreInfo) internal {
        poolManager.unlock(abi.encode(to, payerOrigin, moreInfo));
    }

    function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
        (address to, uint256 payerOrigin, bytes memory moreInfo) = abi.decode(data, (address, uint256, bytes));

        PathKey[] memory pathKeys = abi.decode(moreInfo, (PathKey[]));

        uint256 firstAmountIn;

        if (pathKeys[0].inputCurrency.isAddressZero()) {
            firstAmountIn = IERC20(WETH).balanceOf(address(this));
            IWETH(WETH).withdraw(firstAmountIn);
        } else {
            firstAmountIn = pathKeys[0].inputCurrency.balanceOfSelf();
        }

        uint256 settledAmount = _settle(pathKeys[0].inputCurrency, firstAmountIn);

        (uint256 actualAmountIn, uint256 actualAmountOut) = _swap(pathKeys, settledAmount);
        require(actualAmountOut > 0, "Amount must be positive");
        require(actualAmountIn <= settledAmount, "AmountIn exceeds settledAmount");

        _refundUnused(pathKeys[0].inputCurrency, payerOrigin);
        _takeOutput(pathKeys[pathKeys.length - 1].intermediateCurrency, to, actualAmountOut);

        return "";
    }

    // ──────────────────────── Refund + Take ────────────────────────

    function _refundUnused(Currency inputCurrency, uint256 payerOrigin) internal {
        address _payerOrigin = address(uint160(payerOrigin & ADDRESS_MASK));
        int256 inputDelta = poolManager.currencyDelta(address(this), inputCurrency);
        if (inputDelta > 0) {
            poolManager.take(inputCurrency, _payerOrigin, uint256(inputDelta));
        }
    }

    function _takeOutput(Currency outputCurrency, address to, uint256 amount) internal {
        if (outputCurrency.isAddressZero()) {
            poolManager.take(outputCurrency, address(this), amount);
            IWETH(WETH).deposit{value: amount}();
            SafeERC20.safeTransfer(IERC20(WETH), to, amount);
        } else {
            poolManager.take(outputCurrency, to, amount);
        }
    }

    // ──────────────────────── Swap ────────────────────────

    function _getPoolAndSwapDirection(PathKey memory params)
        internal
        pure
        returns (PoolKey memory poolKey, bool zeroForOne)
    {
        (Currency currency0, Currency currency1) = lessThan(params.inputCurrency, params.intermediateCurrency)
            ? (params.inputCurrency, params.intermediateCurrency)
            : (params.intermediateCurrency, params.inputCurrency);
        zeroForOne = equals(params.inputCurrency, currency0);
        poolKey = PoolKey(currency0, currency1, params.fee, params.tickSpacing, IHooks(params.hook));
    }

    function _swap(PathKey[] memory pathKeys, uint256 firstAmountIn)
        internal
        returns (uint256 actualAmountIn, uint256 actualAmountOut)
    {
        BalanceDelta swapDelta;
        int256 amountIn = int256(firstAmountIn);

        for (uint256 i = 0; i < pathKeys.length; i++) {
            (PoolKey memory poolKey, bool zeroForOne) = _getPoolAndSwapDirection(pathKeys[i]);
            amountIn = -amountIn;
            swapDelta = poolManager.swap(
                poolKey,
                IPoolManager.SwapParams({
                    zeroForOne: zeroForOne,
                    amountSpecified: amountIn,
                    sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_PRICE + 1 : MAX_SQRT_PRICE - 1
                }),
                pathKeys[i].hookData
            );

            int128 amountSpecifiedActual = (zeroForOne == (amountIn < 0)) ? swapDelta.amount0() : swapDelta.amount1();
            if (amountSpecifiedActual != amountIn) revert NotEnoughLiquidity(poolKey.toId());

            amountIn = zeroForOne ? int256(swapDelta.amount1()) : int256(swapDelta.amount0());

            if (i == 0) {
                actualAmountIn =
                    zeroForOne ? uint256(-int256(swapDelta.amount0())) : uint256(-int256(swapDelta.amount1()));
            }
            if (i == pathKeys.length - 1) {
                actualAmountOut = uint256(amountIn);
            }
        }
    }

    // ──────────────────────── Settle ────────────────────────

    function _settle(Currency currency, uint256 amount) internal returns (uint256 paid) {
        if (amount == 0) return 0;
        poolManager.sync(currency);
        if (currency.isAddressZero()) {
            paid = poolManager.settle{value: amount}();
        } else {
            currency.transfer(address(poolManager), amount);
            paid = poolManager.settle();
        }
    }

    receive() external payable {}
}
Chain explorer2292msChain node73ms