// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @notice Sepolia-only prototype. A client-owned wallet with explicitly revocable company transfers. /// @dev No arbitrary calls, token allowances, upgrades or owner replacement are exposed. contract ClientWallet is ReentrancyGuard { using SafeERC20 for IERC20; address public immutable owner; address public immutable company; bool public companyEnabled; error TestnetOnly(); error InvalidAddress(); error InvalidAmount(); error OwnerOnly(); error NotAuthorized(); error TransferFailed(); event CompanyAccessChanged(address indexed owner, address indexed company, bool enabled); event NativeDeposited(address indexed sender, uint256 amount); event AssetTransferred(address indexed actor, address indexed token, address indexed recipient, uint256 amount); constructor(address companyAddress, bool enableCompany) payable { if (block.chainid != 11155111) revert TestnetOnly(); if (companyAddress == address(0)) revert InvalidAddress(); owner = msg.sender; company = companyAddress; companyEnabled = enableCompany; emit CompanyAccessChanged(owner, company, enableCompany); if (msg.value > 0) emit NativeDeposited(msg.sender, msg.value); } modifier onlyOwner() { if (msg.sender != owner) revert OwnerOnly(); _; } modifier authorized() { if (msg.sender != owner && !(companyEnabled && msg.sender == company)) revert NotAuthorized(); _; } /// @notice Only the client can enable or revoke company transfers. No funds move in this call. function setCompanyEnabled(bool enabled) external onlyOwner { companyEnabled = enabled; emit CompanyAccessChanged(owner, company, enabled); } /// @notice Owner or enabled company may transfer ETH to any nonzero recipient. This is irreversible once confirmed. function transferNative(address payable recipient, uint256 amount) external authorized nonReentrant { _validate(recipient, amount); (bool success,) = recipient.call{value: amount}(""); if (!success) revert TransferFailed(); emit AssetTransferred(msg.sender, address(0), recipient, amount); } /// @notice Supports standard ERC20 transfers. No allowance from the client's external wallet is requested. function transferToken(address token, address recipient, uint256 amount) external authorized nonReentrant { _validate(recipient, amount); if (token == address(0) || token == address(this)) revert InvalidAddress(); IERC20(token).safeTransfer(recipient, amount); emit AssetTransferred(msg.sender, token, recipient, amount); } function _validate(address recipient, uint256 amount) private view { if (recipient == address(0) || recipient == address(this)) revert InvalidAddress(); if (amount == 0) revert InvalidAmount(); } receive() external payable { emit NativeDeposited(msg.sender, msg.value); } }