// SPDX-License-Identifier: MIT pragma solidity 0.8.30; interface IERC20Escrow { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } /// @notice Fixed-price escrow for digital goods. The buyer or the disclosed administrator resolves payment. /// @dev No upgrade, recipient change, admin change, arbitrary withdrawal or automatic timeout payout. /// Refunded code inventory is never recycled: a buyer may already have seen the code. contract SwitchEscrowFlow { enum Status { None, Held, Disputed, Released, Refunded } struct Order { uint64 paidAt; Status status; } address public immutable creator; address public immutable token; address public immutable administrator; uint256 public immutable price; bytes32 public immutable manifestHash; uint256 public constant releaseDelay = 72 hours; uint256 public immutable stock; // zero means unlimited file purchases uint256 public sold; uint256 public totalHeld; address[] public recipients; uint16[] public shares; mapping(address => Order) public orders; mapping(address => uint256) public purchaseNumber; bool private locked; event Purchased(address indexed buyer, address indexed token, uint256 amount, bytes32 manifestHash); event DisputeOpened(address indexed buyer); event Released(address indexed buyer, address indexed resolvedBy, uint256 amount); event Refunded(address indexed buyer, address indexed resolvedBy, uint256 amount); event Distributed(address indexed buyer, address indexed recipient, uint256 amount); modifier nonReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } modifier onlyAdmin() { require(msg.sender == administrator, "Administrator only"); _; } constructor(address token_, uint256 price_, address[] memory recipients_, uint16[] memory shares_, bytes32 hash_, uint256 stock_, address admin_) { require(admin_ != address(0) && admin_ != address(this), "Invalid administrator"); require(stock_ <= 100, "Invalid stock"); require(token_.code.length > 0, "Token has no code"); require(price_ > 0 && price_ <= type(uint256).max / 10000, "Invalid price"); require(hash_ != bytes32(0), "Missing manifest"); require(recipients_.length > 0 && recipients_.length <= 4 && recipients_.length == shares_.length, "Invalid recipients"); uint256 total; for (uint256 i; i < recipients_.length; ++i) { require(recipients_[i] != address(0) && recipients_[i] != address(this), "Invalid recipient"); require(shares_[i] > 0, "Empty share"); for (uint256 j; j < i; ++j) require(recipients_[j] != recipients_[i], "Duplicate recipient"); total += shares_[i]; } require(total == 10000, "Shares must total 100%"); creator = msg.sender; token = token_; price = price_; manifestHash = hash_; recipients = recipients_; shares = shares_; stock = stock_; administrator = admin_; } function purchased(address buyer) external view returns (bool) { Status status = orders[buyer].status; return status == Status.Held || status == Status.Disputed || status == Status.Released; } function pay() external nonReentrant { require(orders[msg.sender].status == Status.None, "Order already exists"); require(stock == 0 || sold < stock, "Sold out"); orders[msg.sender] = Order(uint64(block.timestamp), Status.Held); purchaseNumber[msg.sender] = ++sold; totalHeld += price; uint256 beforeBalance = IERC20Escrow(token).balanceOf(address(this)); _call(abi.encodeCall(IERC20Escrow.transferFrom, (msg.sender, address(this), price))); require(IERC20Escrow(token).balanceOf(address(this)) == beforeBalance + price, "Unsupported transfer fee"); emit Purchased(msg.sender, token, price, manifestHash); } function confirmReceipt() external nonReentrant { _release(msg.sender); } function openDispute() external nonReentrant { require(orders[msg.sender].status == Status.Held, "Order is not held"); orders[msg.sender].status = Status.Disputed; emit DisputeOpened(msg.sender); } function adminRelease(address buyer) external onlyAdmin nonReentrant { Order memory order = orders[buyer]; require(order.status == Status.Disputed || (order.status == Status.Held && block.timestamp >= uint256(order.paidAt) + releaseDelay), "Wait 72 hours or resolve a dispute"); _release(buyer); } function adminRefund(address buyer) external onlyAdmin nonReentrant { _requireHeld(buyer); orders[buyer].status = Status.Refunded; totalHeld -= price; uint256 beforeBalance = IERC20Escrow(token).balanceOf(address(this)); _transferExact(buyer, price); require(IERC20Escrow(token).balanceOf(address(this)) == beforeBalance - price, "Unexpected balance"); emit Refunded(buyer, msg.sender, price); } function _requireHeld(address buyer) private view { Status status = orders[buyer].status; require(status == Status.Held || status == Status.Disputed, "Order is already settled or missing"); } function _release(address buyer) private { _requireHeld(buyer); orders[buyer].status = Status.Released; totalHeld -= price; uint256 beforeBalance = IERC20Escrow(token).balanceOf(address(this)); uint256 remaining = price; for (uint256 i; i < recipients.length; ++i) { uint256 amount = i == recipients.length - 1 ? remaining : price * shares[i] / 10000; remaining -= amount; if (amount > 0) _transferExact(recipients[i], amount); emit Distributed(buyer, recipients[i], amount); } require(IERC20Escrow(token).balanceOf(address(this)) == beforeBalance - price, "Unexpected balance"); emit Released(buyer, msg.sender, price); } function _transferExact(address recipient, uint256 amount) private { uint256 beforeRecipient = IERC20Escrow(token).balanceOf(recipient); _call(abi.encodeCall(IERC20Escrow.transfer, (recipient, amount))); require(IERC20Escrow(token).balanceOf(recipient) == beforeRecipient + amount, "Recipient amount mismatch"); } function _call(bytes memory data) private { (bool ok, bytes memory result) = token.call(data); require(ok && (result.length == 0 || (result.length == 32 && abi.decode(result, (bool)))), "Token transfer failed"); } }