Robinhood MainnetChain ID: 4663
Contract detail

BatchRouterV2

BatchRouterV2 is a verified Robinhood Chain smart contract at 0x1ba18cc1…6fc0c52d with 0 indexed transactions.

Address type
Smart contract
Transactions
0
History coverage
No indexed activity found
Verification
Verified
Compiler
v0.8.26+commit.8a97fa7a
Verification match
full
Creation transaction
0xc3677a37…8b625a3e
Created at block
#28494420
Language
solidity
EVM version
cancun
License
none
Compiler optimization
Enabled · 200 runs
Verified contract

Contract source code

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

import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {ERC721Holder} from "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol";
import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {INFTAMMVault} from "../interfaces/INFTAMMVault.sol";
import {BatchTooLarge, SlippageExceeded, ZeroAddress, TokenMismatch, UnknownMarket} from "../libs/Errors.sol";

interface IFactoryMarketsView {
    function isRegisteredMarket(address collection_, address token_, address ammVault_) external view returns (bool);
}

/// @title BatchRouterV2
/// @notice Drop-in replacement for BatchRouter with dedicated TokenMismatch
///         error instead of reusing SlippageExceeded for token validation.
///         Additionally validates ammVault/collection/token against factory-registered markets.
contract BatchRouterV2 is ReentrancyGuard, ERC721Holder {
    using SafeERC20 for IERC20;

    uint256 public constant MAX_BATCH_SIZE = 20;
    address public immutable factory;

    constructor(address factory_) {
        if (factory_ == address(0)) revert ZeroAddress();
        factory = factory_;
    }

    event BatchSold(address indexed seller, address indexed ammVault, address indexed token, uint256 count, uint256 totalPayout);
    event BatchBought(address indexed buyer, address indexed ammVault, address indexed token, uint256 count, uint256 totalCost);

    function batchSellNFT(
        address ammVault,
        address collection,
        address token,
        uint256[] calldata tokenIds,
        uint256[] calldata minPayouts,
        uint256 minTotalPayout
    )
        external
        payable
        nonReentrant
    {
        if (ammVault == address(0) || collection == address(0) || token == address(0)) revert ZeroAddress();
        uint256 length = tokenIds.length;
        if (length == 0 || length > MAX_BATCH_SIZE || minPayouts.length != length) revert BatchTooLarge();

        IERC721 nft = IERC721(collection);
        address actualToken = INFTAMMVault(ammVault).token();
        if (actualToken != token) revert TokenMismatch(actualToken, token);
        if (!_isRegisteredMarket(ammVault, collection, actualToken)) {
            revert UnknownMarket(ammVault, collection, actualToken);
        }

        uint256 perSwapFee = _perSwapFee(ammVault);
        uint256 ethNeeded = perSwapFee * length;
        if (msg.value < ethNeeded) revert SlippageExceeded();

        IERC20 outToken = IERC20(actualToken);
        uint256 beforeBalance = outToken.balanceOf(address(this));
        for (uint256 i = 0; i < length; i++) {
            nft.safeTransferFrom(msg.sender, address(this), tokenIds[i]);
            nft.approve(ammVault, tokenIds[i]);
            INFTAMMVault(ammVault).sellNFT{value: perSwapFee}(tokenIds[i], minPayouts[i]);
        }
        uint256 totalPayout = outToken.balanceOf(address(this)) - beforeBalance;
        if (totalPayout < minTotalPayout) revert SlippageExceeded();

        outToken.safeTransfer(msg.sender, totalPayout);
        if (msg.value > ethNeeded) {
            (bool ok,) = msg.sender.call{value: msg.value - ethNeeded}("");
            require(ok, "eth refund failed");
        }
        emit BatchSold(msg.sender, ammVault, actualToken, length, totalPayout);
    }

    function batchBuySpecificNFT(
        address ammVault,
        address token,
        uint256[] calldata tokenIds,
        uint256[] calldata maxCosts,
        uint256 maxTotalCost
    )
        external
        payable
        nonReentrant
    {
        if (ammVault == address(0) || token == address(0)) revert ZeroAddress();
        uint256 length = tokenIds.length;
        if (length == 0 || length > MAX_BATCH_SIZE || maxCosts.length != length) revert BatchTooLarge();
        IERC721 nft = IERC721(INFTAMMVault(ammVault).collection());

        address actualToken = INFTAMMVault(ammVault).token();
        if (actualToken != token) revert TokenMismatch(actualToken, token);
        if (!_isRegisteredMarket(ammVault, address(nft), actualToken)) {
            revert UnknownMarket(ammVault, address(nft), actualToken);
        }

        uint256 perSwapFee = _perSwapFee(ammVault);
        uint256 ethNeeded = perSwapFee * length;
        if (msg.value < ethNeeded) revert SlippageExceeded();

        IERC20 inToken = IERC20(actualToken);
        uint256 totalMax;
        for (uint256 i = 0; i < length; i++) {
            totalMax += maxCosts[i];
        }
        if (totalMax > maxTotalCost) revert SlippageExceeded();
        inToken.safeTransferFrom(msg.sender, address(this), maxTotalCost);
        inToken.forceApprove(ammVault, maxTotalCost);

        uint256 beforeBalance = inToken.balanceOf(address(this));
        for (uint256 i = 0; i < length; i++) {
            INFTAMMVault(ammVault).buySpecificNFT{value: perSwapFee}(tokenIds[i], maxCosts[i]);
        }

        uint256 spent = beforeBalance - inToken.balanceOf(address(this));
        for (uint256 i = 0; i < length; i++) {
            nft.safeTransferFrom(address(this), msg.sender, tokenIds[i]);
        }
        uint256 refund = maxTotalCost - spent;
        if (refund > 0) {
            inToken.safeTransfer(msg.sender, refund);
        }
        if (msg.value > ethNeeded) {
            (bool ok,) = msg.sender.call{value: msg.value - ethNeeded}("");
            require(ok, "eth refund failed");
        }
        emit BatchBought(msg.sender, ammVault, actualToken, length, spent);
    }

    function _isRegisteredMarket(address ammVault, address collection, address token) internal view returns (bool) {
        return IFactoryMarketsView(factory).isRegisteredMarket(collection, token, ammVault);
    }

    /// @dev Robinhood Chain vaults charge a flat oracle-priced ETH fee per swap;
    /// earlier vault generations don't expose robinhoodSwapFeeWei() at all, so a
    /// missing selector means "no fee" rather than a revert. Keeps one router
    /// source deployable against any vault generation.
    function _perSwapFee(address ammVault) internal view returns (uint256) {
        try INFTAMMVault(ammVault).robinhoodSwapFeeWei() returns (uint256 fee) {
            return fee;
        } catch {
            return 0;
        }
    }
}