// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface IArcVaultLottery { function CHAIN_ID() external view returns (uint256); function SNAPSHOT_DOMAIN() external view returns (bytes32); function initializer() external view returns (address); function token() external view returns (address); function tokenCodeHash() external view returns (bytes32); function tokensPerTicket() external view returns (uint256); function epochZero() external view returns (uint64); function totalDeposited() external view returns (uint256); function depositFees() external payable; } /// @notice Queues native USDC forwarded by an Argus creator wallet, plus donations. /// @dev This vault does not claim Argus fees or prove that the creator forwarded all /// fees. Native and ERC-20 USDC on Arc are one balance; forwarding the native balance /// also includes ERC-20 transfers that did not call receive(). No swap is performed. contract ArcFeeVault is ReentrancyGuard { uint256 public constant CHAIN_ID = 5042; uint256 public constant TICKET_UNIT = 100_000 ether; bytes32 public constant SNAPSHOT_DOMAIN = keccak256("POTLET_ARC_TICKET_SNAPSHOT_V1"); uint16 public constant jackpotBps = 10000; address public immutable initializer; address public immutable creator; IArcVaultLottery public lottery; address public boundToken; bytes32 public tokenCodeHash; bytes32 public lotteryCodeHash; uint256 public boundTokensPerTicket; uint64 public boundEpochZero; uint256 public totalReceivedViaCalls; uint256 public totalCreatorFeesReceived; uint256 public totalForwarded; error InvalidConfiguration(); error NotInitializer(); error NotCreator(); error AlreadyBound(); error EmptyDeposit(); error InvalidLottery(); error InvalidToken(); error RuntimeCodeHashMismatch(); error LotteryConfigurationMismatch(); error DepositAccountingMismatch(); event CreatorFeesReceived(address indexed creator, uint256 amount); event DonationReceived(address indexed sender, uint256 amount); event LotteryBound(address indexed lottery, address indexed token, uint256 tokensPerTicket, bytes32 lotteryRuntimeHash, bytes32 tokenRuntimeHash, uint64 epochZero); event FeesForwarded(address indexed caller, address indexed lottery, uint256 amount); constructor(address initializer_, address creator_) { if (block.chainid != CHAIN_ID || initializer_ == address(0) || creator_ == address(0) || initializer_ == address(this) || creator_ == address(this)) revert InvalidConfiguration(); initializer = initializer_; creator = creator_; } receive() external payable nonReentrant { _donate(); } function donate() external payable nonReentrant { _donate(); } function _donate() private { if (msg.value == 0) revert EmptyDeposit(); totalReceivedViaCalls += msg.value; emit DonationReceived(msg.sender, msg.value); } /// @notice Records a creator's transfer, without asserting its Argus provenance. function depositCreatorFees() external payable nonReentrant { if (msg.sender != creator) revert NotCreator(); if (msg.value == 0) revert EmptyDeposit(); totalCreatorFeesReceived += msg.value; totalReceivedViaCalls += msg.value; emit CreatorFeesReceived(msg.sender, msg.value); } /// @notice Binding never moves funds, including funds queued before activation. function bindLottery(address lottery_, address expectedToken, uint256 expectedUnit, bytes32 expectedLotteryRuntimeHash, bytes32 expectedTokenRuntimeHash) external nonReentrant { if (msg.sender != initializer) revert NotInitializer(); if (address(lottery) != address(0)) revert AlreadyBound(); if (lottery_.code.length == 0 || lottery_ == address(this)) revert InvalidLottery(); if (expectedToken.code.length == 0 || expectedToken == address(this) || expectedToken == lottery_) revert InvalidToken(); if (expectedUnit != TICKET_UNIT) revert InvalidConfiguration(); IArcVaultLottery target = IArcVaultLottery(lottery_); uint64 start = target.epochZero(); if (start == 0 || start % 300 != 0) revert LotteryConfigurationMismatch(); _checkConfiguration(target, expectedToken, expectedUnit, expectedLotteryRuntimeHash, expectedTokenRuntimeHash, start); target.totalDeposited(); lottery = target; boundToken = expectedToken; tokenCodeHash = expectedTokenRuntimeHash; lotteryCodeHash = expectedLotteryRuntimeHash; boundTokensPerTicket = expectedUnit; boundEpochZero = start; emit LotteryBound(lottery_, expectedToken, expectedUnit, expectedLotteryRuntimeHash, expectedTokenRuntimeHash, start); } function pendingBalance() external view returns (uint256) { return address(this).balance; } /// @notice Anyone may flush the full balance; preactivation funds stay queued. function forwardPending() external nonReentrant returns (uint256 amount) { IArcVaultLottery target = lottery; if (address(target) == address(0) || block.timestamp < boundEpochZero) return 0; amount = address(this).balance; if (amount == 0) return 0; _checkConfiguration(target, boundToken, boundTokensPerTicket, lotteryCodeHash, tokenCodeHash, boundEpochZero); uint256 beforeDeposited = target.totalDeposited(); totalForwarded += amount; target.depositFees{value: amount}(); if (target.totalDeposited() != beforeDeposited + amount) revert DepositAccountingMismatch(); emit FeesForwarded(msg.sender, address(target), amount); } function _checkConfiguration(IArcVaultLottery target, address token_, uint256 unit, bytes32 lotteryHash, bytes32 tokenHash, uint64 start) private view { if (block.chainid != CHAIN_ID || address(target).codehash != lotteryHash || token_.codehash != tokenHash) revert RuntimeCodeHashMismatch(); if (target.CHAIN_ID() != CHAIN_ID || target.SNAPSHOT_DOMAIN() != SNAPSHOT_DOMAIN || target.initializer() != initializer || target.token() != token_ || target.tokenCodeHash() != tokenHash || target.tokensPerTicket() != unit || target.epochZero() != start) revert LotteryConfigurationMismatch(); } }