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;
}
}
}