FlowPadCurveRouter

0x9ebdf5862d696961c9f4f305b2955b8482bbc661

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,074 bytes
ABI entries
21
2 read · 7 write
License
none

Contract information

Address
0x9ebdf5862d696961c9f4f305b2955b8482bbc661
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xf79e49f3b4…56fd2e5f76

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (2)

hook()address
poolManager()address

Events (1)

Trade

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "hook_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientPayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "Trade",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "buyFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyTokens",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "buyTokensFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethSpent",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "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": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "ethTo",
        "type": "address"
      }
    ],
    "name": "sellFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "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.26;

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title FlowPadCurveRouter (v4 single-sided model)
/// @notice EXPERIMENTAL, LOCAL-ONLY. Our own v4 swap router — the parity
/// counterpart to the production factory's buy/buyFor/buyTokens/sell/sellFor.
/// Because a FlowPad token is a REAL v4 pool from block one, external
/// aggregators (GMGN, Axiom, Universal Router) can already trade it with their
/// OWN routers; this one exists for our frontend and for buyer attribution,
/// adding slippage (minOut), deadlines, recipient delivery (Weg 1) and native
/// refunds. It never custodies tokens: buys `take` directly to the recipient,
/// sells `transferFrom` the seller straight into the PoolManager.
contract FlowPadCurveRouter is IUnlockCallback {
    using SafeERC20 for IERC20;
    using CurrencyLibrary for Currency;

    error NotPoolManager();
    error DeadlineExpired();
    error ZeroAddress();
    error ZeroAmount();
    error SlippageExceeded();
    error InsufficientPayment();
    error EthTransferFailed();
    error NotOwner();

    /// @notice One clean trade record per swap through this router — the end
    /// user (buy recipient / seller), the direction and both amounts — so the
    /// indexer can build price / volume / candles / the trade feed for v4 tokens.
    event Trade(address indexed token, address indexed trader, bool isBuy, uint256 ethAmount, uint256 tokenAmount);

    IPoolManager public immutable poolManager;
    IHooks public immutable hook;

    enum Kind {
        BUY_EXACT_IN,
        BUY_EXACT_OUT,
        SELL_EXACT_IN
    }

    struct Call {
        Kind kind;
        address token;
        uint256 amount; // exact-in: ETH/token in; exact-out: tokens out
        uint256 limit; // minOut (buy/sell) or maxEthIn (exact-out)
        address payer; // who funds ETH / receives ETH refund
        address recipient; // who receives tokens (buy) or ETH (sell)
        address from; // token source for sells
    }

    constructor(IPoolManager poolManager_, IHooks hook_) {
        poolManager = poolManager_;
        hook = hook_;
    }

    // ------------------------------------------------------------------- buys

    function buy(address token, uint256 minTokensOut, uint256 deadline) external payable returns (uint256) {
        return buyFor(token, minTokensOut, deadline, msg.sender);
    }

    /// @notice Buy delivering tokens directly to `recipient`; msg.sender pays
    /// and gets any refund (Weg 1 — max-wallet/creator exemption apply to the
    /// recipient).
    function buyFor(address token, uint256 minTokensOut, uint256 deadline, address recipient)
        public
        payable
        returns (uint256)
    {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (recipient == address(0)) revert ZeroAddress();
        if (msg.value == 0) revert ZeroAmount();
        bytes memory ret = poolManager.unlock(
            abi.encode(Call(Kind.BUY_EXACT_IN, token, msg.value, minTokensOut, msg.sender, recipient, address(0)))
        );
        return abi.decode(ret, (uint256));
    }

    function buyTokens(address token, uint256 tokensOut, uint256 deadline) external payable returns (uint256) {
        return buyTokensFor(token, tokensOut, deadline, msg.sender);
    }

    /// @notice Exact-OUTPUT buy: receive exactly `tokensOut`, pay up to msg.value
    /// (the MAX button); the unused remainder is refunded.
    function buyTokensFor(address token, uint256 tokensOut, uint256 deadline, address recipient)
        public
        payable
        returns (uint256 ethSpent)
    {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (recipient == address(0)) revert ZeroAddress();
        if (tokensOut == 0) revert ZeroAmount();
        bytes memory ret = poolManager.unlock(
            abi.encode(Call(Kind.BUY_EXACT_OUT, token, tokensOut, msg.value, msg.sender, recipient, address(0)))
        );
        return abi.decode(ret, (uint256));
    }

    // ------------------------------------------------------------------ sells

    function sell(address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline) external returns (uint256) {
        return sellFor(token, tokensIn, minEthOut, deadline, msg.sender, msg.sender);
    }

    /// @notice Sell your own `tokensIn` (pulled from `from == msg.sender`) and
    /// direct the ETH to `ethTo`. `from` is required to equal the caller so a
    /// standing allowance to this router can never be spent by a third party;
    /// delegated sells (an aggregator selling for a user) must use the user's
    /// OWN router + the pool directly, or a signed permit.
    function sellFor(address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline, address from, address ethTo)
        public
        returns (uint256)
    {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (from != msg.sender) revert NotOwner();
        if (ethTo == address(0)) revert ZeroAddress();
        if (tokensIn == 0) revert ZeroAmount();
        bytes memory ret =
            poolManager.unlock(abi.encode(Call(Kind.SELL_EXACT_IN, token, tokensIn, minEthOut, from, ethTo, from)));
        return abi.decode(ret, (uint256));
    }

    // --------------------------------------------------------------- unlock

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        Call memory c = abi.decode(data, (Call));
        PoolKey memory key = _key(c.token);

        if (c.kind == Kind.SELL_EXACT_IN) {
            BalanceDelta ds = poolManager.swap(
                key,
                SwapParams({
                    zeroForOne: false,
                    amountSpecified: -int256(c.amount),
                    sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
                }),
                ""
            );
            uint256 tokIn = ds.amount1() < 0 ? uint256(uint128(-ds.amount1())) : 0;
            uint256 ethOut = ds.amount0() > 0 ? uint256(uint128(ds.amount0())) : 0;
            if (ethOut < c.limit) revert SlippageExceeded();
            // settle tokens straight from the seller into the PoolManager
            poolManager.sync(key.currency1);
            IERC20(c.token).safeTransferFrom(c.from, address(poolManager), tokIn);
            poolManager.settle();
            poolManager.take(key.currency0, c.recipient, ethOut);
            emit Trade(c.token, c.from, false, ethOut, tokIn);
            return abi.encode(ethOut);
        }

        bool exactOut = c.kind == Kind.BUY_EXACT_OUT;
        BalanceDelta d = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: true,
                amountSpecified: exactOut ? int256(c.amount) : -int256(c.amount),
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );
        uint256 spent = d.amount0() < 0 ? uint256(uint128(-d.amount0())) : 0;
        uint256 tokensOut = d.amount1() > 0 ? uint256(uint128(d.amount1())) : 0;

        if (exactOut) {
            if (spent > c.limit) revert InsufficientPayment(); // c.limit == msg.value
            if (tokensOut < c.amount) revert SlippageExceeded(); // AMM couldn't fill the exact output
        } else {
            if (tokensOut < c.limit) revert SlippageExceeded();
        }

        if (spent > 0) poolManager.settle{value: spent}();
        if (tokensOut > 0) poolManager.take(key.currency1, c.recipient, tokensOut);
        emit Trade(c.token, c.recipient, true, spent, tokensOut);
        uint256 funded = exactOut ? c.limit : c.amount; // msg.value in both cases
        uint256 refund = funded - spent;
        if (refund > 0) {
            (bool ok,) = c.payer.call{value: refund}("");
            if (!ok) revert EthTransferFailed();
        }
        return abi.encode(exactOut ? spent : tokensOut);
    }

    function _key(address token) internal view returns (PoolKey memory) {
        return PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(token),
            fee: 0,
            tickSpacing: 60,
            hooks: hook
        });
    }

    receive() external payable {}
}
Chain explorer5393msChain node80ms