// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @notice Sells one indivisible basket of 2–8 onchain assets for USDG or native ETH. /// @dev Arbitrary asset contracts can misrepresent their balances or transfer rules; /// buyers must independently verify every asset contract before purchasing. contract MintideBasketDualEscrow is IERC721Receiver, IERC1155Receiver, ReentrancyGuard { using SafeERC20 for IERC20; uint8 public constant ERC20_ASSET = 1; uint8 public constant ERC721_ASSET = 2; uint8 public constant ERC1155_ASSET = 3; uint8 public constant MAX_ASSETS = 8; struct Asset { uint8 kind; address token; uint256 id; uint256 amount; } struct Order { address seller; uint256 price; uint64 expiry; address reservedBuyer; bool payInEth; bool active; } IERC20 public immutable settlementToken; address public immutable feeRecipient; uint16 public immutable feeBps; uint256 public nextOrderId = 1; mapping(uint256 => Order) public orders; mapping(uint256 => Asset[]) private orderAssets; uint8 private expectedKind; address private expectedToken; address private expectedSeller; uint256 private expectedId; uint256 private expectedAmount; event BasketCreated(uint256 indexed orderId, address indexed seller, uint256 price, uint64 expiry, address reservedBuyer, bool payInEth); event BasketAsset(uint256 indexed orderId, uint8 indexed index, uint8 kind, address token, uint256 id, uint256 amount); event BasketFilled(uint256 indexed orderId, address indexed buyer, uint256 price, uint256 fee, bool payInEth); event BasketCancelled(uint256 indexed orderId); constructor(IERC20 token_, address feeRecipient_, uint16 feeBps_) { require(address(token_).code.length != 0 && feeRecipient_ != address(0), "invalid constructor"); require(feeBps_ <= 100, "fee above 1%"); settlementToken = token_; feeRecipient = feeRecipient_; feeBps = feeBps_; } function createBasket( Asset[] calldata assets, uint256 price, uint64 expiry, address reservedBuyer, bool payInEth ) external nonReentrant returns (uint256 orderId) { require(assets.length >= 2 && assets.length <= MAX_ASSETS, "basket size"); require(price != 0, "zero price"); require(expiry > block.timestamp && expiry <= block.timestamp + 90 days, "invalid expiry"); require(reservedBuyer != msg.sender, "seller reserved"); orderId = nextOrderId++; orders[orderId] = Order(msg.sender, price, expiry, reservedBuyer, payInEth, true); for (uint256 i; i < assets.length; ++i) { Asset calldata asset = assets[i]; require(asset.token.code.length != 0 && asset.amount != 0, "invalid asset"); require(asset.kind >= ERC20_ASSET && asset.kind <= ERC1155_ASSET, "invalid kind"); require(asset.kind != ERC20_ASSET || (asset.id == 0 && asset.token != address(settlementToken)), "invalid ERC20"); require(asset.kind != ERC721_ASSET || asset.amount == 1, "invalid ERC721 amount"); for (uint256 j; j < i; ++j) { Asset calldata prior = assets[j]; require(prior.kind != asset.kind || prior.token != asset.token || prior.id != asset.id, "duplicate asset"); } _lock(asset); orderAssets[orderId].push(asset); emit BasketAsset(orderId, uint8(i), asset.kind, asset.token, asset.id, asset.amount); } emit BasketCreated(orderId, msg.sender, price, expiry, reservedBuyer, payInEth); } function buy(uint256 orderId) external payable nonReentrant { Order storage order = orders[orderId]; require(order.active && block.timestamp <= order.expiry, "inactive or expired"); require(msg.sender != order.seller, "seller cannot buy"); require(order.reservedBuyer == address(0) || order.reservedBuyer == msg.sender, "reserved order"); order.active = false; uint256 fee = order.price * feeBps / 10_000; if (order.payInEth) { require(msg.value == order.price, "ETH amount mismatch"); _sendEth(order.seller, order.price - fee); if (fee != 0) _sendEth(feeRecipient, fee); } else { require(msg.value == 0, "ETH not accepted"); uint256 beforeBalance = settlementToken.balanceOf(address(this)); settlementToken.safeTransferFrom(msg.sender, address(this), order.price); require(settlementToken.balanceOf(address(this)) - beforeBalance == order.price, "payment mismatch"); settlementToken.safeTransfer(order.seller, order.price - fee); if (fee != 0) settlementToken.safeTransfer(feeRecipient, fee); } Asset[] storage assets = orderAssets[orderId]; for (uint256 i; i < assets.length; ++i) _deliver(assets[i], msg.sender); emit BasketFilled(orderId, msg.sender, order.price, fee, order.payInEth); } function _sendEth(address receiver, uint256 value) private { (bool success,) = payable(receiver).call{value: value}(""); require(success, "ETH payment failed"); } function cancel(uint256 orderId) external nonReentrant { Order storage order = orders[orderId]; require(order.active, "inactive order"); require(msg.sender == order.seller || block.timestamp > order.expiry, "not cancellable"); order.active = false; Asset[] storage assets = orderAssets[orderId]; for (uint256 i; i < assets.length; ++i) _deliver(assets[i], order.seller); emit BasketCancelled(orderId); } function getAssets(uint256 orderId) external view returns (Asset[] memory) { return orderAssets[orderId]; } function _lock(Asset calldata asset) private { if (asset.kind == ERC20_ASSET) { IERC20 token = IERC20(asset.token); uint256 beforeBalance = token.balanceOf(address(this)); token.safeTransferFrom(msg.sender, address(this), asset.amount); require(token.balanceOf(address(this)) - beforeBalance == asset.amount, "ERC20 not received"); } else { expectedKind = asset.kind; expectedToken = asset.token; expectedSeller = msg.sender; expectedId = asset.id; expectedAmount = asset.amount; if (asset.kind == ERC721_ASSET) { IERC721 token = IERC721(asset.token); token.safeTransferFrom(msg.sender, address(this), asset.id); require(token.ownerOf(asset.id) == address(this), "ERC721 not received"); } else { IERC1155 token = IERC1155(asset.token); uint256 beforeBalance = token.balanceOf(address(this), asset.id); token.safeTransferFrom(msg.sender, address(this), asset.id, asset.amount, ""); require(token.balanceOf(address(this), asset.id) - beforeBalance == asset.amount, "ERC1155 not received"); } delete expectedKind; delete expectedToken; delete expectedSeller; delete expectedId; delete expectedAmount; } } function _deliver(Asset storage asset, address receiver) private { if (asset.kind == ERC20_ASSET) { IERC20 token = IERC20(asset.token); uint256 beforeBalance = token.balanceOf(receiver); token.safeTransfer(receiver, asset.amount); require(token.balanceOf(receiver) - beforeBalance == asset.amount, "ERC20 delivery mismatch"); } else if (asset.kind == ERC721_ASSET) { IERC721(asset.token).safeTransferFrom(address(this), receiver, asset.id); require(IERC721(asset.token).ownerOf(asset.id) == receiver, "ERC721 delivery mismatch"); } else { IERC1155 token = IERC1155(asset.token); uint256 beforeBalance = token.balanceOf(receiver, asset.id); token.safeTransferFrom(address(this), receiver, asset.id, asset.amount, ""); require(token.balanceOf(receiver, asset.id) - beforeBalance == asset.amount, "ERC1155 delivery mismatch"); } } function onERC721Received(address, address from, uint256 id, bytes calldata) external view override returns (bytes4) { require(expectedKind == ERC721_ASSET && msg.sender == expectedToken && from == expectedSeller && id == expectedId, "unexpected ERC721"); return IERC721Receiver.onERC721Received.selector; } function onERC1155Received(address, address from, uint256 id, uint256 amount, bytes calldata) external view override returns (bytes4) { require(expectedKind == ERC1155_ASSET && msg.sender == expectedToken && from == expectedSeller && id == expectedId && amount == expectedAmount, "unexpected ERC1155"); return IERC1155Receiver.onERC1155Received.selector; } function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external pure override returns (bytes4) { revert("batch transfer unsupported"); } function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(IERC721Receiver).interfaceId; } }