// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {VestaDeposit, IERC20} from "./VestaDeposit.sol"; /// @title VestaDepositFactory /// @notice Gives every invoice its own deposit address, and forwards the USDC /// that arrives there to the merchant (97%) and the Vesta fee wallet (3%). /// @dev Holds no funds and has no owner, no pause and no withdraw function. /// `sweep` is open to anyone: the money can only ever move to the /// merchant and the fee wallet, so an open sweep means funds can never /// be stuck waiting for Vesta to act. /// /// Chain-independence: the factory is deployed through the standard /// deterministic CREATE2 deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C) /// with a fixed salt, and neither the factory's constructor nor the /// deposit init code contains a token address. So the factory, and every /// deposit address, is the same on every chain where the same fee wallet /// and fee are used. If a customer sends USDC on the wrong network, the /// factory can be deployed there at the same address and the payment /// swept with that chain's USDC. The token to sweep is a parameter of /// `sweep`, which also means a wrong token is forwardable, not stranded. contract VestaDepositFactory { address public immutable feeWallet; uint16 public immutable feeBps; /// @dev Set only for the duration of one sweep; the deposit contract reads it /// from its constructor. Zero at all other times. address private _sweepToken; event DepositSwept( bytes32 indexed invoiceId, address indexed merchant, address indexed token, address depositAddress, uint256 gross, uint256 fee, uint256 merchantAmount ); error ZeroAddress(); error FeeTooHigh(); error NothingToSweep(); error AddressMismatch(); error SweepIncomplete(); constructor(address _feeWallet, uint16 _feeBps) { if (_feeWallet == address(0)) revert ZeroAddress(); if (_feeBps > 1_000) revert FeeTooHigh(); // hard ceiling of 10% feeWallet = _feeWallet; feeBps = _feeBps; } /// @notice The token being swept right now (zero outside a sweep). function sweepToken() external view returns (address) { return _sweepToken; } /// @notice The address a customer should send USDC to for this invoice. /// @dev Pure arithmetic: the same inputs always give the same address, /// on-chain and off-chain, whether or not anything is deployed yet. /// Independent of the token. function depositAddress(bytes32 invoiceId, address merchant) public view returns (address) { return address( uint160( uint256( keccak256( abi.encodePacked( bytes1(0xff), address(this), invoiceId, initCodeHash(merchant) ) ) ) ) ); } /// @notice Hash of the init code for a merchant's deposit contracts. /// @dev Published so the backend can derive addresses without an RPC call. function initCodeHash(address merchant) public view returns (bytes32) { return keccak256(_initCode(merchant)); } /// @notice Forward everything of `token` sitting at an invoice's deposit address. /// @dev Callable by anyone. Reverts if there is nothing there. `token` is /// USDC in normal operation; any other ERC-20 sent by mistake can be /// forwarded the same way (it goes to the merchant and the fee wallet /// in the same 97/3 split). function sweep(bytes32 invoiceId, address merchant, address token) external returns (address deposit, uint256 gross, uint256 fee, uint256 merchantAmount) { if (token == address(0) || merchant == address(0)) revert ZeroAddress(); deposit = depositAddress(invoiceId, merchant); gross = IERC20(token).balanceOf(deposit); if (gross == 0) revert NothingToSweep(); fee = (gross * feeBps) / 10_000; merchantAmount = gross - fee; _sweepToken = token; VestaDeposit child = new VestaDeposit{salt: invoiceId}(merchant, feeWallet, feeBps); _sweepToken = address(0); if (address(child) != deposit) revert AddressMismatch(); if (IERC20(token).balanceOf(deposit) != 0) revert SweepIncomplete(); emit DepositSwept(invoiceId, merchant, token, deposit, gross, fee, merchantAmount); } function _initCode(address merchant) private view returns (bytes memory) { return abi.encodePacked( type(VestaDeposit).creationCode, abi.encode(merchant, feeWallet, feeBps) ); } }