// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {Ownable2Step} from "@openzeppelin/access/Ownable2Step.sol"; import {ReentrancyGuard} from "@openzeppelin/security/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; interface IPonsFeeEscrow { function claim() external; function claimToken(address token) external; } interface IPonsFactory { function transferCreatorFeeRecipient(address token, address newRecipient) external; } /// @title SHERWOOD Zcash mining procurement vault /// @notice One owner-managed treasury. Funds buy Equihash hashpower offchain; holders have no redemption rights. /// @dev Non-upgradeable. Uses the escrow and factory belonging to the specific Pons launch. contract SherwoodVault is Ownable2Step, ReentrancyGuard { using SafeERC20 for IERC20; address public procurementWallet; address public immutable feeEscrow; address public immutable ponsFactory; uint256 public totalNativeWithdrawn; error InvalidAddress(); error InvalidAmount(); error NativeTransferFailed(); error OwnershipRequired(); event NativeReceived(address indexed sender, uint256 amount); event NativeWithdrawn(address indexed operator, address indexed recipient, uint256 amount); event TokenWithdrawn(address indexed token, address indexed recipient, uint256 amount); event PonsFeesClaimed(address indexed asset, uint256 amount); event ProcurementWalletChanged(address indexed previousWallet, address indexed newWallet); event PonsFeeRecipientRedirected(address indexed token, address indexed recipient); constructor(address initialOwner, address procurementWallet_, address feeEscrow_, address ponsFactory_) { if (initialOwner == address(0) || initialOwner == address(this)) revert InvalidAddress(); if (feeEscrow_.code.length == 0 || ponsFactory_.code.length == 0) revert InvalidAddress(); _transferOwnership(initialOwner); _setProcurementWallet(procurementWallet_); feeEscrow = feeEscrow_; ponsFactory = ponsFactory_; } receive() external payable { emit NativeReceived(msg.sender, msg.value); } /// @notice Includes direct deposits and forced ETH; ERC20 assets are accounted separately. function totalNativeReceived() external view returns (uint256) { return address(this).balance + totalNativeWithdrawn; } function withdrawNative(uint256 amount) external onlyOwner nonReentrant { if (amount == 0 || amount > address(this).balance) revert InvalidAmount(); address recipient = procurementWallet; totalNativeWithdrawn += amount; (bool ok,) = payable(recipient).call{value: amount}(""); if (!ok) revert NativeTransferFailed(); emit NativeWithdrawn(msg.sender, recipient, amount); } function withdrawToken(address token, uint256 amount) external onlyOwner nonReentrant { if (token == address(0) || token.code.length == 0) revert InvalidAddress(); if (amount == 0 || amount > IERC20(token).balanceOf(address(this))) revert InvalidAmount(); address recipient = procurementWallet; IERC20(token).safeTransfer(recipient, amount); emit TokenWithdrawn(token, recipient, amount); } /// @notice Claims only this vault's already-settled escrow balance. Does not sweep or trade. /// @param asset address(0) for ETH, otherwise the specific quote or vested ERC20 asset. function claimPonsFees(address asset) external onlyOwner nonReentrant { uint256 beforeBalance; uint256 afterBalance; if (asset == address(0)) { beforeBalance = address(this).balance; IPonsFeeEscrow(feeEscrow).claim(); afterBalance = address(this).balance; } else { if (asset.code.length == 0) revert InvalidAddress(); beforeBalance = IERC20(asset).balanceOf(address(this)); IPonsFeeEscrow(feeEscrow).claimToken(asset); afterBalance = IERC20(asset).balanceOf(address(this)); } emit PonsFeesClaimed(asset, afterBalance - beforeBalance); } function setProcurementWallet(address wallet) external onlyOwner { _setProcurementWallet(wallet); } function _setProcurementWallet(address wallet) private { if (wallet == address(0) || wallet == address(this)) revert InvalidAddress(); emit ProcurementWalletChanged(procurementWallet, wallet); procurementWallet = wallet; } /// @notice Lets the owner redirect future Pons fees if the vault is retired. Existing balances stay claimable. function redirectPonsFees(address token, address recipient) external onlyOwner nonReentrant { if (token == address(0) || recipient == address(0)) revert InvalidAddress(); IPonsFactory(ponsFactory).transferCreatorFeeRecipient(token, recipient); emit PonsFeeRecipientRedirected(token, recipient); } function transferOwnership(address newOwner) public override onlyOwner { if (newOwner == address(0) || newOwner == address(this)) revert InvalidAddress(); super.transferOwnership(newOwner); } /// @dev Prevents permanently stranding funds or future Pons fees by renouncing ownership. function renounceOwnership() public view override onlyOwner { revert OwnershipRequired(); } }