Robinhood MainnetChain ID: 4663
Contract detail

EquifoldV4CustomSwapRouter

Live Robinhood Chain account facts for 0x04c817b7…ab170d16 with at least 25 observed transactions. The history total is updating as indexing continues.

Address type
Smart contract
Transactions
25+ observed
First indexed block
#60016243
Latest indexed block
#82382876
History coverage
Recent activity available; total is catching up
Indexed token transfers
14
ERC-20 transfers
14
Verification
Verified
Compiler
0.8.24+commit.e11b9ed9
Verification match
partial
Language
Solidity
EVM version
cancun
License
MIT
Compiler optimization
Enabled · 10000 runs
Deployed bytecode hash
0x3cb77e7b3481d95890087cb3d1fe617687ec5deea0c914468c909b7408a8041f
Related activity

Transactions

Verified contract

Contract source code

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

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { BalanceDelta, BalanceDeltaLibrary } 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";

interface IEquifoldV4CustomModuleFactory {
    function poolKeyOf(
        address token
    ) external view returns (PoolKey memory);
    function quoteOf(
        address token
    ) external view returns (address);
}

/// @title Equifold direct custom-quote swap router
/// @notice Trades a custom-paired launch token directly against its selected ERC-20 quote. It does
///         not silently bridge through USDG: every pool keeps its own volume and fee denomination.
contract EquifoldV4CustomSwapRouter is IUnlockCallback, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using BalanceDeltaLibrary for BalanceDelta;

    error AmountOverflow();
    error DeadlineExpired();
    error IncorrectCaller();
    error InvalidAddress();
    error InvalidRoute();
    error SlippageExceeded();
    error UnknownToken();
    error ZeroAmount();
    error ZeroOutput();

    uint256 private constant MAX_EXACT_IN_AMOUNT = (1 << 255) - 1;

    struct CallbackData {
        PoolKey key;
        bool zeroForOne;
        uint256 amountIn;
        Currency input;
        Currency output;
        address recipient;
    }

    IPoolManager public immutable poolManager;
    IEquifoldV4CustomModuleFactory public immutable moduleFactory;

    constructor(
        IPoolManager poolManager_,
        address moduleFactory_
    ) {
        if (address(poolManager_) == address(0) || moduleFactory_ == address(0)) {
            revert InvalidAddress();
        }
        if (address(poolManager_).code.length == 0 || moduleFactory_.code.length == 0) {
            revert InvalidAddress();
        }
        poolManager = poolManager_;
        moduleFactory = IEquifoldV4CustomModuleFactory(moduleFactory_);
    }

    function buyWithQuote(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory key, address quote) = _launch(token);
        IERC20(quote).safeTransferFrom(msg.sender, address(this), amountIn);
        amountOut = _run(key, Currency.wrap(quote), amountIn, recipient);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    function sellToQuote(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory key,) = _launch(token);
        IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn);
        amountOut = _run(key, Currency.wrap(token), amountIn, recipient);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    function unlockCallback(
        bytes calldata rawData
    ) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert IncorrectCaller();
        CallbackData memory d = abi.decode(rawData, (CallbackData));
        BalanceDelta delta = poolManager.swap(
            d.key,
            SwapParams({
                zeroForOne: d.zeroForOne,
                amountSpecified: _negatedAmountSpecified(d.amountIn),
                sqrtPriceLimitX96: d.zeroForOne
                    ? TickMath.MIN_SQRT_PRICE + 1
                    : TickMath.MAX_SQRT_PRICE - 1
            }),
            ""
        );
        int128 inputDelta = d.zeroForOne ? delta.amount0() : delta.amount1();
        int128 outputDelta = d.zeroForOne ? delta.amount1() : delta.amount0();
        if (inputDelta >= 0 || outputDelta <= 0) revert ZeroOutput();
        // Settle the delta the PoolManager actually consumed. Paying the
        // caller-declared exact-in amount directly is unsafe at a price limit,
        // where a swap may consume less than requested.
        uint256 inputOwed = uint256(uint128(-inputDelta));
        uint256 amountOut = uint256(uint128(outputDelta));
        poolManager.sync(d.input);
        IERC20(Currency.unwrap(d.input)).safeTransfer(address(poolManager), inputOwed);
        poolManager.settle();
        poolManager.take(d.output, d.recipient, amountOut);
        return abi.encode(amountOut, inputOwed);
    }

    function _launch(
        address token
    ) private view returns (PoolKey memory key, address quote) {
        key = moduleFactory.poolKeyOf(token);
        quote = moduleFactory.quoteOf(token);
        if (quote == address(0)) revert UnknownToken();
    }

    function _run(
        PoolKey memory key,
        Currency input,
        uint256 amountIn,
        address recipient
    ) private returns (uint256 amountOut) {
        if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
        bool zeroForOne;
        Currency output;
        if (Currency.unwrap(key.currency0) == Currency.unwrap(input)) {
            zeroForOne = true;
            output = key.currency1;
        } else if (Currency.unwrap(key.currency1) == Currency.unwrap(input)) {
            zeroForOne = false;
            output = key.currency0;
        } else {
            revert InvalidRoute();
        }
        uint256 amountConsumed;
        (amountOut, amountConsumed) = abi.decode(
            poolManager.unlock(
                abi.encode(
                    CallbackData({
                        key: key,
                        zeroForOne: zeroForOne,
                        amountIn: amountIn,
                        input: input,
                        output: output,
                        recipient: recipient
                    })
                )
            ),
            (uint256, uint256)
        );
        // An exact-input swap can stop at the extreme price limit after using
        // only part of the requested amount. Return that unconsumed input to
        // the caller instead of leaving it stranded in this stateless router.
        if (amountConsumed < amountIn) {
            IERC20(Currency.unwrap(input)).safeTransfer(msg.sender, amountIn - amountConsumed);
        }
    }

    function _guard(
        uint256 deadline,
        address recipient,
        uint256 amountIn
    ) private view {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (recipient == address(0)) revert InvalidAddress();
        if (amountIn == 0) revert ZeroAmount();
    }

    function _negatedAmountSpecified(
        uint256 amountIn
    ) private pure returns (int256) {
        if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
        return -int256(amountIn);
    }
}