From da25e2e63f4f8977c16c0ba11ebeb180a821ab60 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 11 Nov 2025 14:40:22 -0400 Subject: [PATCH 01/13] feat: GasTankManager contract --- contracts/contracts/core/GasTankManager.sol | 113 ++++++++++++++++++ .../contracts/core/GasTankManagerStorage.sol | 12 ++ .../contracts/interfaces/IGasTankManager.sol | 64 ++++++++++ 3 files changed, 189 insertions(+) create mode 100644 contracts/contracts/core/GasTankManager.sol create mode 100644 contracts/contracts/core/GasTankManagerStorage.sol create mode 100644 contracts/contracts/interfaces/IGasTankManager.sol diff --git a/contracts/contracts/core/GasTankManager.sol b/contracts/contracts/core/GasTankManager.sol new file mode 100644 index 000000000..a5cbbe331 --- /dev/null +++ b/contracts/contracts/core/GasTankManager.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IGasTankManager} from "../interfaces/IGasTankManager.sol"; +import {GasTankManagerStorage} from "./GasTankManagerStorage.sol"; +import {Errors} from "../utils/Errors.sol"; + +/// @title GasTankManager +/// @notice Coordinates on-demand ETH bridging so EOA gas tank balances stay funded for mev-commit transactions. +/// @dev The RPC provider manages EOA gas tank balances on the Mev Commit chain. +/// @dev Flow overview: +/// - EOA (L1) +/// - Authorizes and sets this contract as a delegate against its own EOA address. (ERC-7702) +/// - Calls `sendMinimumDeposit` to send the initial ETH funds to their gas tank on the Mev Commit chain. +/// - Provider (mev-commit) +/// - Triggers a top-up via `topUpGasTank` when the gas tank balance cannot cover the next mev-commit transaction. This transfer amount is always the difference +/// between the `minDeposit` and the current balance of this contract. The provider is the only one who can trigger a top-up. +/// - These funds are then transferred to the EOA gas tank on the Mev Commit chain. +/// - When a mev-commit transaction is made, the provider deducts the amount needed from the EOA's gas tank. +contract GasTankManager is IGasTankManager, GasTankManagerStorage, Ownable2StepUpgradeable, UUPSUpgradeable { + /// @notice Restricts calls to those triggered internally via `onlyThisEOA`. + modifier onlyThisEOA() { + require(msg.sender == address(this), NotThisEOA(msg.sender, address(this))); + _; + } + + /// @notice Locks the implementation upon deployment. + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Accepts direct ETH deposits and forwards them to the provider. + receive() external payable { + _sendFundsToProvider(msg.value); + } + + /// @notice Reverts on any call data to keep the interface surface narrow. + fallback() external payable { + revert Errors.InvalidFallback(); + } + + /// @notice Initializes ownership and baseline deposit requirement. + /// @param _owner EOA managed by the RPC provider. + /// @param _minDeposit Initial deposit requirement in wei. + function initialize(address _owner, uint256 _minDeposit) external initializer { + minDeposit = _minDeposit; + __Ownable_init(_owner); + __UUPSUpgradeable_init(); + } + + /// @inheritdoc IGasTankManager + function setMinimumDeposit(uint256 _minDeposit) external onlyOwner { + minDeposit = _minDeposit; + emit MinimumDepositSet(_minDeposit); + } + + /// @inheritdoc IGasTankManager + function topUpGasTank(uint256 _gasTankBalance) external onlyOwner { + uint256 minDeposit_ = minDeposit; + uint256 available = address(this).balance; + uint256 needed = minDeposit_ - _gasTankBalance; + + _validateTopUp(_gasTankBalance, available, needed, minDeposit_); + + _sendFundsToProvider(needed); + emit GasTankToppedUp(needed); + } + + /// @inheritdoc IGasTankManager + function fundGasTank() external payable { + _sendFundsToProvider(msg.value); + } + + /// @inheritdoc IGasTankManager + function sendMinimumDeposit() external payable onlyThisEOA { + require(msg.value >= minDeposit, InsufficientFundsSent(msg.value, minDeposit)); + _sendFundsToProvider(minDeposit); + } + + /// @notice Restricts upgradeability to the owner. + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/foundry/api/upgrades + /// solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address) internal override onlyOwner {} + + /// @notice Forwards ETH to the configured provider address. + /// @dev Reverts when the provider rejects the transfer. + /// @param _amount Amount of wei to transfer. + function _sendFundsToProvider(uint256 _amount) internal { + address provider = owner(); + require(provider != address(0), ProviderNotSet(provider)); + + (bool success,) = provider.call{value: _amount}(""); + require(success, GasTankTopUpFailed(provider, _amount)); + } + + /// @notice Validates the proposed refill before forwarding funds. + /// @dev Applies guardrails to protect configured thresholds. + /// @param _gasTankBalance Current gas tank balance. + /// @param _available Current contract (EOA) balance. + /// @param _needed Amount required to reach the `minDeposit`. + /// @param _minDeposit Minimum deposit requirement in wei. + function _validateTopUp(uint256 _gasTankBalance, uint256 _available, uint256 _needed, uint256 _minDeposit) + internal + view + { + require(_gasTankBalance < _minDeposit, GasTankBalanceIsSufficient(_gasTankBalance, _minDeposit)); + require(_available > _needed, InsufficientEOABalance(address(this), _available, _needed)); + } +} diff --git a/contracts/contracts/core/GasTankManagerStorage.sol b/contracts/contracts/core/GasTankManagerStorage.sol new file mode 100644 index 000000000..d7e4ebe25 --- /dev/null +++ b/contracts/contracts/core/GasTankManagerStorage.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +/// @title GasTankManager storage layout +/// @notice Keeps state variables isolated for upgradeable deployments. +abstract contract GasTankManagerStorage { + /// @dev Minimum wei balance the provider should top up to + uint256 public minDeposit; + + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps + uint256[48] private __gap; +} diff --git a/contracts/contracts/interfaces/IGasTankManager.sol b/contracts/contracts/interfaces/IGasTankManager.sol new file mode 100644 index 000000000..55abee6b0 --- /dev/null +++ b/contracts/contracts/interfaces/IGasTankManager.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +/** + * @title IGasTankManager + * @dev Interface for GasTankManager + */ +interface IGasTankManager { + /// @dev Event to log successful update of the minimum deposit requirement. + event MinimumDepositSet(uint256 indexed minDeposit); + + /// @dev Event to log successful gas tank top up. + event GasTankToppedUp(uint256 indexed transferAmount); + + /// @notice Raised when a call expected from the contract itself is not. + /// @param msgSender Address that invoked the call. + /// @param thisAddress Address of the contract guard. + error NotThisEOA(address msgSender, address thisAddress); + + /// @notice Raised when the contract balance cannot satisfy a refill. + /// @param eoaAddress L1 EOA address. + /// @param available Wei currently available on the L1 EOA address. + /// @param needed Wei required to meet the minimum deposit requirement. + error InsufficientEOABalance(address eoaAddress, uint256 available, uint256 needed); + + /// @notice Raised when forwarding funds to the provider reverts. + /// @param rpcProvider Destination address for the transfer. + /// @param transferAmount Amount of wei attempted. + error GasTankTopUpFailed(address rpcProvider, uint256 transferAmount); + + /// @notice Raised when a gas tank balance already meets the minimum deposit requirement. + /// @param currentBalance Gas tank balance reported by the provider. + /// @param minDeposit Minimum deposit requirement. + error GasTankBalanceIsSufficient(uint256 currentBalance, uint256 minDeposit); + + /// @notice Raised when the initial minimum deposit is not sufficient. + /// @param sentAmount Amount of wei sent. + /// @param requiredAmount Amount of wei required. + error InsufficientFundsSent(uint256 sentAmount, uint256 requiredAmount); + + /// @notice Raised when the provider address is not set. + /// @param provider Provider address. + error ProviderNotSet(address provider); + + /// @notice Updates the minimum deposit requirement. + /// @param minDeposit New minimum balance expressed in wei. + /// @dev Only the owner can call this function. + function setMinimumDeposit(uint256 minDeposit) external; + + /// @notice Requests a top-up when the gas tank's balance is below the minimum deposit requirement. + /// @param gasTankBalance Balance that the provider currently holds on the gas tank. + /// @dev Only the owner can call this function. + /// @dev Reverts if the current gas tank balance is greater than or equal to the minimum deposit requirement. + /// @dev Reverts if the contract balance is less than the amount needed to reach the minimum deposit requirement. + /// @dev Always transfer the difference between the minimum deposit requirement and the current gas tank balance. + function topUpGasTank(uint256 gasTankBalance) external; + + /// @notice Allows anyone to contribute funds. Forwards them to the provider immediately. + function fundGasTank() external payable; + + /// @notice Sets the initial minimum deposit requirement. Forwards them to the provider immediately. + /// @dev Reverts if the amount sent is less than the baseline deposit. + function sendMinimumDeposit() external payable; +} From 81b1dea4d3770ab5a360dbc000334ba867563c10 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 11 Nov 2025 20:20:31 -0400 Subject: [PATCH 02/13] refactor: update comments and minimum deposit logic --- contracts/contracts/core/GasTankManager.sol | 86 +++++++------------ .../contracts/interfaces/IGasTankManager.sol | 36 +++----- 2 files changed, 46 insertions(+), 76 deletions(-) diff --git a/contracts/contracts/core/GasTankManager.sol b/contracts/contracts/core/GasTankManager.sol index a5cbbe331..8709739f7 100644 --- a/contracts/contracts/core/GasTankManager.sol +++ b/contracts/contracts/core/GasTankManager.sol @@ -8,17 +8,14 @@ import {GasTankManagerStorage} from "./GasTankManagerStorage.sol"; import {Errors} from "../utils/Errors.sol"; /// @title GasTankManager -/// @notice Coordinates on-demand ETH bridging so EOA gas tank balances stay funded for mev-commit transactions. -/// @dev The RPC provider manages EOA gas tank balances on the Mev Commit chain. +/// @notice Coordinates on-demand ETH Transfers to the RPC Service for EOA custodial gas tanks. /// @dev Flow overview: -/// - EOA (L1) -/// - Authorizes and sets this contract as a delegate against its own EOA address. (ERC-7702) -/// - Calls `sendMinimumDeposit` to send the initial ETH funds to their gas tank on the Mev Commit chain. -/// - Provider (mev-commit) -/// - Triggers a top-up via `topUpGasTank` when the gas tank balance cannot cover the next mev-commit transaction. This transfer amount is always the difference -/// between the `minDeposit` and the current balance of this contract. The provider is the only one who can trigger a top-up. -/// - These funds are then transferred to the EOA gas tank on the Mev Commit chain. -/// - When a mev-commit transaction is made, the provider deducts the amount needed from the EOA's gas tank. +/// - EOA (Prerequisites for use) +/// - Authorizes and sets this contract as a delegate against its own EOA address. (ERC-7702 compliant) +/// - Sends the initial minimum deposit via `initializeGasTank` to the RPC Service. +/// - RPC Service +/// - Triggers `topUpGasTank`, transferring the `minDeposit` when the gas tank requires funding. +/// - These funds are then transferred to the RPC Service's custodial gas tank. contract GasTankManager is IGasTankManager, GasTankManagerStorage, Ownable2StepUpgradeable, UUPSUpgradeable { /// @notice Restricts calls to those triggered internally via `onlyThisEOA`. modifier onlyThisEOA() { @@ -26,26 +23,28 @@ contract GasTankManager is IGasTankManager, GasTankManagerStorage, Ownable2StepU _; } + modifier isValidCaller() { + require(msg.sender == address(this) || msg.sender == owner(), NotValidCaller(msg.sender)); + _; + } + /// @notice Locks the implementation upon deployment. - /// @dev See https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } - /// @notice Accepts direct ETH deposits and forwards them to the provider. - receive() external payable { - _sendFundsToProvider(msg.value); - } + /// @notice Accepts direct ETH deposits. + receive() external payable {} /// @notice Reverts on any call data to keep the interface surface narrow. fallback() external payable { revert Errors.InvalidFallback(); } - /// @notice Initializes ownership and baseline deposit requirement. - /// @param _owner EOA managed by the RPC provider. - /// @param _minDeposit Initial deposit requirement in wei. + /// @notice Initializes ownership and the minimum deposit requirement. + /// @param _owner EOA managed by the RPC Service. + /// @param _minDeposit Minimum deposit requirement. function initialize(address _owner, uint256 _minDeposit) external initializer { minDeposit = _minDeposit; __Ownable_init(_owner); @@ -59,55 +58,34 @@ contract GasTankManager is IGasTankManager, GasTankManagerStorage, Ownable2StepU } /// @inheritdoc IGasTankManager - function topUpGasTank(uint256 _gasTankBalance) external onlyOwner { - uint256 minDeposit_ = minDeposit; - uint256 available = address(this).balance; - uint256 needed = minDeposit_ - _gasTankBalance; - - _validateTopUp(_gasTankBalance, available, needed, minDeposit_); - - _sendFundsToProvider(needed); - emit GasTankToppedUp(needed); + function topUpGasTank() external onlyOwner { + _sendFundsToProvider(minDeposit); } /// @inheritdoc IGasTankManager - function fundGasTank() external payable { - _sendFundsToProvider(msg.value); + function initializeGasTank() external onlyThisEOA { + _sendFundsToProvider(minDeposit); } /// @inheritdoc IGasTankManager - function sendMinimumDeposit() external payable onlyThisEOA { - require(msg.value >= minDeposit, InsufficientFundsSent(msg.value, minDeposit)); - _sendFundsToProvider(minDeposit); + function fundGasTank() external payable isValidCaller { + _sendFundsToProvider(msg.value); } - /// @notice Restricts upgradeability to the owner. - /// @dev See https://docs.openzeppelin.com/upgrades-plugins/foundry/api/upgrades /// solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address) internal override onlyOwner {} - /// @notice Forwards ETH to the configured provider address. - /// @dev Reverts when the provider rejects the transfer. - /// @param _amount Amount of wei to transfer. + /// @notice Forwards ETH to the rpcService. + /// @param _amount Gas tank top-up amount. function _sendFundsToProvider(uint256 _amount) internal { - address provider = owner(); - require(provider != address(0), ProviderNotSet(provider)); + address rpcService = owner(); - (bool success,) = provider.call{value: _amount}(""); - require(success, GasTankTopUpFailed(provider, _amount)); - } + require(rpcService != address(0), ProviderNotSet(rpcService)); + require(_amount > 0 && address(this).balance >= _amount, InvalidAmount()); + + (bool success,) = rpcService.call{value: _amount}(""); + require(success, GasTankTopUpFailed(rpcService, _amount)); - /// @notice Validates the proposed refill before forwarding funds. - /// @dev Applies guardrails to protect configured thresholds. - /// @param _gasTankBalance Current gas tank balance. - /// @param _available Current contract (EOA) balance. - /// @param _needed Amount required to reach the `minDeposit`. - /// @param _minDeposit Minimum deposit requirement in wei. - function _validateTopUp(uint256 _gasTankBalance, uint256 _available, uint256 _needed, uint256 _minDeposit) - internal - view - { - require(_gasTankBalance < _minDeposit, GasTankBalanceIsSufficient(_gasTankBalance, _minDeposit)); - require(_available > _needed, InsufficientEOABalance(address(this), _available, _needed)); + emit GasTankToppedUp(address(this), msg.sender, _amount); } } diff --git a/contracts/contracts/interfaces/IGasTankManager.sol b/contracts/contracts/interfaces/IGasTankManager.sol index 55abee6b0..031a415bb 100644 --- a/contracts/contracts/interfaces/IGasTankManager.sol +++ b/contracts/contracts/interfaces/IGasTankManager.sol @@ -10,18 +10,19 @@ interface IGasTankManager { event MinimumDepositSet(uint256 indexed minDeposit); /// @dev Event to log successful gas tank top up. - event GasTankToppedUp(uint256 indexed transferAmount); + event GasTankToppedUp(address indexed smartAccount, address indexed caller, uint256 indexed amount); /// @notice Raised when a call expected from the contract itself is not. /// @param msgSender Address that invoked the call. /// @param thisAddress Address of the contract guard. error NotThisEOA(address msgSender, address thisAddress); - /// @notice Raised when the contract balance cannot satisfy a refill. - /// @param eoaAddress L1 EOA address. - /// @param available Wei currently available on the L1 EOA address. - /// @param needed Wei required to meet the minimum deposit requirement. - error InsufficientEOABalance(address eoaAddress, uint256 available, uint256 needed); + /// @notice Raised when an invalid caller is detected. + /// @param caller Address that invoked the call. + error NotValidCaller(address caller); + + /// @notice Raised when an invalid amount is detected. + error InvalidAmount(); /// @notice Raised when forwarding funds to the provider reverts. /// @param rpcProvider Destination address for the transfer. @@ -33,11 +34,6 @@ interface IGasTankManager { /// @param minDeposit Minimum deposit requirement. error GasTankBalanceIsSufficient(uint256 currentBalance, uint256 minDeposit); - /// @notice Raised when the initial minimum deposit is not sufficient. - /// @param sentAmount Amount of wei sent. - /// @param requiredAmount Amount of wei required. - error InsufficientFundsSent(uint256 sentAmount, uint256 requiredAmount); - /// @notice Raised when the provider address is not set. /// @param provider Provider address. error ProviderNotSet(address provider); @@ -47,18 +43,14 @@ interface IGasTankManager { /// @dev Only the owner can call this function. function setMinimumDeposit(uint256 minDeposit) external; - /// @notice Requests a top-up when the gas tank's balance is below the minimum deposit requirement. - /// @param gasTankBalance Balance that the provider currently holds on the gas tank. - /// @dev Only the owner can call this function. - /// @dev Reverts if the current gas tank balance is greater than or equal to the minimum deposit requirement. - /// @dev Reverts if the contract balance is less than the amount needed to reach the minimum deposit requirement. - /// @dev Always transfer the difference between the minimum deposit requirement and the current gas tank balance. - function topUpGasTank(uint256 gasTankBalance) external; + /// @notice RPC requested top-up of the gas tank. + /// @dev Always transfers the minimum deposit requirement. + function topUpGasTank() external; - /// @notice Allows anyone to contribute funds. Forwards them to the provider immediately. + /// @notice Allows anyone to fund the gas tank. function fundGasTank() external payable; - /// @notice Sets the initial minimum deposit requirement. Forwards them to the provider immediately. - /// @dev Reverts if the amount sent is less than the baseline deposit. - function sendMinimumDeposit() external payable; + /// @notice Initializes the gas tank with the minimum deposit. + /// @dev Only the EOA can call this function. + function initializeGasTank() external; } From 68c5eba5d245e751f6a53289a3b79bf7c21e9dc9 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 12 Nov 2025 19:47:55 -0400 Subject: [PATCH 03/13] refactor: remove proxy related files --- .../contracts/core/GasTankManagerStorage.sol | 12 ---- .../contracts/interfaces/IGasTankManager.sol | 56 ------------------- 2 files changed, 68 deletions(-) delete mode 100644 contracts/contracts/core/GasTankManagerStorage.sol delete mode 100644 contracts/contracts/interfaces/IGasTankManager.sol diff --git a/contracts/contracts/core/GasTankManagerStorage.sol b/contracts/contracts/core/GasTankManagerStorage.sol deleted file mode 100644 index d7e4ebe25..000000000 --- a/contracts/contracts/core/GasTankManagerStorage.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BSL 1.1 -pragma solidity 0.8.26; - -/// @title GasTankManager storage layout -/// @notice Keeps state variables isolated for upgradeable deployments. -abstract contract GasTankManagerStorage { - /// @dev Minimum wei balance the provider should top up to - uint256 public minDeposit; - - /// @dev See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps - uint256[48] private __gap; -} diff --git a/contracts/contracts/interfaces/IGasTankManager.sol b/contracts/contracts/interfaces/IGasTankManager.sol deleted file mode 100644 index 031a415bb..000000000 --- a/contracts/contracts/interfaces/IGasTankManager.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: BSL 1.1 -pragma solidity 0.8.26; - -/** - * @title IGasTankManager - * @dev Interface for GasTankManager - */ -interface IGasTankManager { - /// @dev Event to log successful update of the minimum deposit requirement. - event MinimumDepositSet(uint256 indexed minDeposit); - - /// @dev Event to log successful gas tank top up. - event GasTankToppedUp(address indexed smartAccount, address indexed caller, uint256 indexed amount); - - /// @notice Raised when a call expected from the contract itself is not. - /// @param msgSender Address that invoked the call. - /// @param thisAddress Address of the contract guard. - error NotThisEOA(address msgSender, address thisAddress); - - /// @notice Raised when an invalid caller is detected. - /// @param caller Address that invoked the call. - error NotValidCaller(address caller); - - /// @notice Raised when an invalid amount is detected. - error InvalidAmount(); - - /// @notice Raised when forwarding funds to the provider reverts. - /// @param rpcProvider Destination address for the transfer. - /// @param transferAmount Amount of wei attempted. - error GasTankTopUpFailed(address rpcProvider, uint256 transferAmount); - - /// @notice Raised when a gas tank balance already meets the minimum deposit requirement. - /// @param currentBalance Gas tank balance reported by the provider. - /// @param minDeposit Minimum deposit requirement. - error GasTankBalanceIsSufficient(uint256 currentBalance, uint256 minDeposit); - - /// @notice Raised when the provider address is not set. - /// @param provider Provider address. - error ProviderNotSet(address provider); - - /// @notice Updates the minimum deposit requirement. - /// @param minDeposit New minimum balance expressed in wei. - /// @dev Only the owner can call this function. - function setMinimumDeposit(uint256 minDeposit) external; - - /// @notice RPC requested top-up of the gas tank. - /// @dev Always transfers the minimum deposit requirement. - function topUpGasTank() external; - - /// @notice Allows anyone to fund the gas tank. - function fundGasTank() external payable; - - /// @notice Initializes the gas tank with the minimum deposit. - /// @dev Only the EOA can call this function. - function initializeGasTank() external; -} From 2c1277fbafd128328d266a3506369a9dfde8b0a6 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 12 Nov 2025 19:49:30 -0400 Subject: [PATCH 04/13] feat: create standard gastank contract --- contracts/contracts/core/GasTankManager.sol | 107 +++++++++----------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/contracts/contracts/core/GasTankManager.sol b/contracts/contracts/core/GasTankManager.sol index 8709739f7..1d046e07c 100644 --- a/contracts/contracts/core/GasTankManager.sol +++ b/contracts/contracts/core/GasTankManager.sol @@ -1,91 +1,84 @@ // SPDX-License-Identifier: BSL 1.1 pragma solidity 0.8.26; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IGasTankManager} from "../interfaces/IGasTankManager.sol"; -import {GasTankManagerStorage} from "./GasTankManagerStorage.sol"; import {Errors} from "../utils/Errors.sol"; /// @title GasTankManager /// @notice Coordinates on-demand ETH Transfers to the RPC Service for EOA custodial gas tanks. -/// @dev Flow overview: -/// - EOA (Prerequisites for use) -/// - Authorizes and sets this contract as a delegate against its own EOA address. (ERC-7702 compliant) -/// - Sends the initial minimum deposit via `initializeGasTank` to the RPC Service. -/// - RPC Service -/// - Triggers `topUpGasTank`, transferring the `minDeposit` when the gas tank requires funding. -/// - These funds are then transferred to the RPC Service's custodial gas tank. -contract GasTankManager is IGasTankManager, GasTankManagerStorage, Ownable2StepUpgradeable, UUPSUpgradeable { - /// @notice Restricts calls to those triggered internally via `onlyThisEOA`. +/// @dev This contract implicitly trusts the RPC_SERVICE address. +contract GasTankManager { + address public immutable RPC_SERVICE; + uint256 public immutable MINIMUM_DEPOSIT; + + event FundsRecovered(address indexed owner, uint256 indexed amount); + event GasTankFunded(address indexed smartAccount, address indexed caller, uint256 indexed amount); + + error FailedToRecoverFunds(address owner, uint256 amount); + error NotValidCaller(address caller); + error InvalidAmount(); + error FailedToFundGasTank(address rpcProvider, uint256 transferAmount); + error RPCServiceNotSet(address provider); + error NotRPCService(address caller); + error InsufficientFunds(uint256 currentBalance, uint256 requiredBalance); + error NotThisEOA(address msgSender, address thisAddress); + error MinimumDepositNotMet(uint256 amountToTransfer, uint256 minimumDeposit); + modifier onlyThisEOA() { require(msg.sender == address(this), NotThisEOA(msg.sender, address(this))); _; } - modifier isValidCaller() { - require(msg.sender == address(this) || msg.sender == owner(), NotValidCaller(msg.sender)); + modifier onlyRPCService() { + require(msg.sender == RPC_SERVICE, NotRPCService(msg.sender)); _; } - /// @notice Locks the implementation upon deployment. - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); + constructor(address rpcService, uint256 _minDeposit) { + require(rpcService != address(0), RPCServiceNotSet(rpcService)); + require(_minDeposit > 0, MinimumDepositNotMet(0, _minDeposit)); + RPC_SERVICE = rpcService; + MINIMUM_DEPOSIT = _minDeposit; } - /// @notice Accepts direct ETH deposits. - receive() external payable {} + receive() external payable { /* ETH transfers allowed. */ } - /// @notice Reverts on any call data to keep the interface surface narrow. fallback() external payable { revert Errors.InvalidFallback(); } - /// @notice Initializes ownership and the minimum deposit requirement. - /// @param _owner EOA managed by the RPC Service. - /// @param _minDeposit Minimum deposit requirement. - function initialize(address _owner, uint256 _minDeposit) external initializer { - minDeposit = _minDeposit; - __Ownable_init(_owner); - __UUPSUpgradeable_init(); - } + /// @notice Recovers funds inadvertently sent to this contract directly. + function recoverFunds() external onlyRPCService { + uint256 balance = address(this).balance; - /// @inheritdoc IGasTankManager - function setMinimumDeposit(uint256 _minDeposit) external onlyOwner { - minDeposit = _minDeposit; - emit MinimumDepositSet(_minDeposit); - } + (bool success,) = RPC_SERVICE.call{value: balance}(""); + require(success, FailedToRecoverFunds(RPC_SERVICE, balance)); - /// @inheritdoc IGasTankManager - function topUpGasTank() external onlyOwner { - _sendFundsToProvider(minDeposit); + emit FundsRecovered(RPC_SERVICE, balance); } - /// @inheritdoc IGasTankManager - function initializeGasTank() external onlyThisEOA { - _sendFundsToProvider(minDeposit); + /// @notice Transfers ETH from the EOA's balance to the Gas RPC Service. + /// @param _amount The amount to fund the gas tank with. + /// @dev Only the EOA can call this function. + function fundGasTank(uint256 _amount) external onlyThisEOA { + require(_amount >= MINIMUM_DEPOSIT, MinimumDepositNotMet(_amount, MINIMUM_DEPOSIT)); + _fundGasTank(_amount); } - /// @inheritdoc IGasTankManager - function fundGasTank() external payable isValidCaller { - _sendFundsToProvider(msg.value); + /// @notice Transfers the minimum deposit amount of ETH from the EOA's balance to the Gas RPC Service. + /// @dev Only the RPC Service can call this function. + function fundGasTank() external onlyRPCService { + _fundGasTank(MINIMUM_DEPOSIT); } - /// solhint-disable-next-line no-empty-blocks - function _authorizeUpgrade(address) internal override onlyOwner {} - - /// @notice Forwards ETH to the rpcService. - /// @param _amount Gas tank top-up amount. - function _sendFundsToProvider(uint256 _amount) internal { - address rpcService = owner(); - - require(rpcService != address(0), ProviderNotSet(rpcService)); - require(_amount > 0 && address(this).balance >= _amount, InvalidAmount()); + /// @dev `fundGasTank` Internal function to fund the gas tank. + function _fundGasTank(uint256 _amountToTransfer) internal { + require(address(this).balance >= _amountToTransfer, InsufficientFunds(address(this).balance, _amountToTransfer)); - (bool success,) = rpcService.call{value: _amount}(""); - require(success, GasTankTopUpFailed(rpcService, _amount)); + (bool success,) = RPC_SERVICE.call{value: _amountToTransfer}(""); + if (!success) { + revert FailedToFundGasTank(RPC_SERVICE, _amountToTransfer); + } - emit GasTankToppedUp(address(this), msg.sender, _amount); + emit GasTankFunded(address(this), msg.sender, _amountToTransfer); } } From 63b7741ca3c750c3db24ad8479bf1d964fd47417 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 12 Nov 2025 19:51:54 -0400 Subject: [PATCH 05/13] test: add unit tests for GasTankManger contract --- contracts/test/core/GasTankManagerTest.sol | 205 +++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 contracts/test/core/GasTankManagerTest.sol diff --git a/contracts/test/core/GasTankManagerTest.sol b/contracts/test/core/GasTankManagerTest.sol new file mode 100644 index 000000000..d05cfbf56 --- /dev/null +++ b/contracts/test/core/GasTankManagerTest.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import {GasTankManager} from "../../contracts/core/GasTankManager.sol"; + +contract GasTankManagerTest is Test { + uint256 public constant ALICE_PK = uint256(0xA11CE); + uint256 public constant BOB_PK = uint256(0xB0B); + uint256 public constant RPC_SERVICE_PK = uint256(0x1234567890); + uint256 public constant MINIMUM_DEPOSIT = 0.01 ether; + address public constant ZERO_ADDRESS = address(0); + bytes32 public constant EMPTY_CODEHASH = keccak256(""); + + address public alice = payable(vm.addr(ALICE_PK)); + address public bob = payable(vm.addr(BOB_PK)); + address public rpcService = payable(vm.addr(RPC_SERVICE_PK)); + + GasTankManager private _gasTankManagerImpl; + + function setUp() public { + vm.deal(alice, 10 ether); + vm.deal(bob, 10 ether); + vm.deal(rpcService, 10 ether); + _gasTankManagerImpl = new GasTankManager(rpcService, MINIMUM_DEPOSIT); + } + + //=======================TESTS======================= + + function testSetsDelegationCodeAtAddress() public { + // Initial code is empty + assertEq(alice.code.length, 0); + + // Set delegation as the GasTankManager + _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); + + assertEq(alice.codehash, _delegateCodeHash(address(_gasTankManagerImpl))); + assertEq(alice.code.length, 23); + } + + function testRemovesDelegationCodeAtAddress() public { + // Set delegation as the GasTankManager + _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); + assertEq(alice.codehash, _delegateCodeHash(address(_gasTankManagerImpl))); + assertEq(alice.code.length, 23); + + // Remove delegation + _signAndAttachDelegation(ZERO_ADDRESS, ALICE_PK); + assertEq(alice.codehash, EMPTY_CODEHASH); + assertEq(alice.code.length, 0); + } + + //=======================TESTS FOR IMPROPER CALLS TO THE GAS TANK MANAGER======================= + + function testFallbackRevert() public { + bytes memory badData = + abi.encodeWithSelector(GasTankManager.recoverFunds.selector, address(0x55555), 1 ether, 1 ether, 1 ether); + vm.prank(alice); + (bool success,) = address(_gasTankManagerImpl).call{value: 1 ether}(badData); + assertFalse(success); + } + + function testReceiveNoRevert() public { + uint256 beforeBalance = alice.balance; + vm.prank(bob); + (bool success,) = address(alice).call{value: 1 ether}(""); + assertTrue(success); + uint256 afterBalance = alice.balance; + assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); + } + + function testFundsSentDirectlyToDelegateAddress() public { + uint256 beforeBalance = address(_gasTankManagerImpl).balance; + + vm.prank(bob); + (bool success,) = address(_gasTankManagerImpl).call{value: 1 ether}(""); + assertTrue(success); + + uint256 afterBalance = address(_gasTankManagerImpl).balance; + assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); + } + + function testWithdrawsFundsDirectlyFromDelegateAddress() public { + uint256 gasTankBeforeBalance = address(_gasTankManagerImpl).balance; + uint256 depositAmount = 1 ether; + + vm.prank(bob); + (bool success,) = address(_gasTankManagerImpl).call{value: depositAmount}(""); + assertTrue(success); + + uint256 gasTankAfterBalance = address(_gasTankManagerImpl).balance; + assertEq(gasTankAfterBalance, gasTankBeforeBalance + depositAmount, "balance not increased"); + + vm.prank(rpcService); + uint256 rpcServiceBeforeBalance = rpcService.balance; + _gasTankManagerImpl.recoverFunds(); + uint256 rpcServiceAfterBalance = rpcService.balance; + + assertEq(address(_gasTankManagerImpl).balance, 0, "funds not drained"); + assertEq(rpcServiceAfterBalance, rpcServiceBeforeBalance + depositAmount, "balance not recovered"); + } + + function testRevertsWhenRecoverFundsIsCalledByUnknownCaller() public { + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotRPCService.selector, bob)); + _gasTankManagerImpl.recoverFunds(); + } + + //=======================TESTS FOR FUNDING THE GAS TANK======================= + + function testRpcServiceFundsMinimumDeposit() public { + _delegate(); + + uint256 rpcBalanceBefore = rpcService.balance; + _expectGasTankFunded(rpcService, MINIMUM_DEPOSIT); + + vm.prank(rpcService); + GasTankManager(payable(alice)).fundGasTank(); + + assertEq(rpcService.balance, rpcBalanceBefore + MINIMUM_DEPOSIT, "rpc balance not increased"); + } + + function testRpcServiceFundRevertsWhenCallerNotRpcService() public { + _delegate(); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotRPCService.selector, alice)); + GasTankManager(payable(alice)).fundGasTank(); + } + + function testRpcServiceFundRevertsWhenInsufficientBalance() public { + vm.deal(alice, MINIMUM_DEPOSIT - 1); + _delegate(); + + vm.prank(rpcService); + vm.expectRevert( + abi.encodeWithSelector(GasTankManager.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + ); + GasTankManager(payable(alice)).fundGasTank(); + } + + function testEOAFundsGasTank() public { + uint256 amount = 1 ether; + _delegate(); + + uint256 rpcBalanceBefore = rpcService.balance; + _expectGasTankFunded(alice, amount); + + vm.prank(alice); + GasTankManager(payable(alice)).fundGasTank(amount); + + assertEq(rpcService.balance, rpcBalanceBefore + amount, "rpc balance not increased"); + } + + function testEOAFundRevertsBelowMinimumDeposit() public { + _delegate(); + uint256 belowMinimumDeposit = MINIMUM_DEPOSIT - 1 wei; + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(GasTankManager.MinimumDepositNotMet.selector, belowMinimumDeposit, MINIMUM_DEPOSIT) + ); + GasTankManager(payable(alice)).fundGasTank(belowMinimumDeposit); + } + + function testEOAFundRevertsWhenCallerNotEOA() public { + _delegate(); + + vm.prank(rpcService); + vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotThisEOA.selector, rpcService, alice)); + GasTankManager(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + } + + function testEOAFundRevertsWhenInsufficientBalance() public { + vm.deal(alice, MINIMUM_DEPOSIT - 1); + _delegate(); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(GasTankManager.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + ); + GasTankManager(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + } + + //=======================HELPERS======================= + + function _delegate() internal { + _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); + } + + function _expectGasTankFunded(address caller, uint256 amount) internal { + vm.expectEmit(true, true, true, true); + emit GasTankManager.GasTankFunded(alice, caller, amount); + } + + function _signAndAttachDelegation(address contractAddress, uint256 pk) internal { + vm.prank(alice); + vm.signAndAttachDelegation(contractAddress, pk); + vm.stopPrank(); + } + + function _delegateCodeHash(address contractAddress) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"ef0100", contractAddress)); + } +} From f1257f310e8e32e26f4354536b21ea7cb915d833 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 12 Nov 2025 20:21:38 -0400 Subject: [PATCH 06/13] docs: add GasTankManager overview --- contracts/doc/GasTankManager.md | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 contracts/doc/GasTankManager.md diff --git a/contracts/doc/GasTankManager.md b/contracts/doc/GasTankManager.md new file mode 100644 index 000000000..9a7e7f3fb --- /dev/null +++ b/contracts/doc/GasTankManager.md @@ -0,0 +1,50 @@ +# GasTankManager Contract Documentation + +## Overview + +The `GasTankManager` contract coordinates on-demand ETH transfers from user EOAs to an RPC service-managed EOA for custodial gas tank management. This contract enables automatic gas tank top-ups using the ERC-7702 standard, allowing users to delegate smart contract functionality to their EOA addresses without requiring a contract wallet. + +## Purpose + +The contract facilitates a custodial gas tank system where: +- Users deposit ETH that is transferred to an RPC service-managed EOA +- The RPC service maintains an off-chain ledger tracking each user's custodial balance +- When users transact with the FAST RPC service, their off-chain ledger balance is debited to cover transaction costs +- The RPC service can automatically top up user accounts when balances run low + +## Architecture + +### Key Components + +1. **RPC Service EOA**: A single EOA address managed by the RPC service that receives all gas tank deposits +2. **Off-Chain Ledger**: The RPC service maintains a ledger tracking each user's custodial balance +3. **ERC-7702 Delegation**: Users delegate their EOA to the `GasTankManager` contract, enabling smart contract functionality on their EOA address +4. **Minimum Deposit**: Immutable minimum amount that must be transferred in each top-up operation + +### How It Works + +1. **User Authorization** (One-time setup): + - User authorizes the `GasTankManager` contract using ERC-7702 + - User sends a network transaction to attach the delegation + - After delegation, the user's EOA can execute contract functions as if it were a smart contract + +2. **Initial Funding**: + - User calls `fundGasTank(uint256 _amount)` with their desired initial deposit + - Amount must be >= `MINIMUM_DEPOSIT` + - ETH is transferred from user's EOA to the RPC service EOA + - RPC service updates off-chain ledger to reflect the deposit + +3. **Automatic Top-Ups**: + - When a user's off-chain ledger balance drops below threshold, RPC service calls `fundGasTank()` + - This always transfers exactly `MINIMUM_DEPOSIT` amount + - Transfer occurs directly from user's EOA balance (if sufficient funds available) + - No user interaction required - fully automated + - No need for `maxTransferAllowance` as RPC is restricted to minimum amount only + +4. **Off-Chain Ledger Operations**: + - RPC service tracks user balances in off-chain ledger + - When users transact with FAST RPC service, ledger debits their account + - When balance is low, RPC service triggers automatic top-up + - All transfers go to the single RPC service EOA + + From f106e0d91d8e34f63ff22955b527b582d2db61d5 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Fri, 14 Nov 2025 08:50:40 -0400 Subject: [PATCH 07/13] refactor: rename contract --- ...asTankManager.sol => GasTankDepositor.sol} | 4 +- ...{GasTankManager.md => GasTankDepositor.md} | 8 +-- ...nagerTest.sol => GasTankDepositorTest.sol} | 72 +++++++++---------- 3 files changed, 42 insertions(+), 42 deletions(-) rename contracts/contracts/core/{GasTankManager.sol => GasTankDepositor.sol} (98%) rename contracts/doc/{GasTankManager.md => GasTankDepositor.md} (79%) rename contracts/test/core/{GasTankManagerTest.sol => GasTankDepositorTest.sol} (65%) diff --git a/contracts/contracts/core/GasTankManager.sol b/contracts/contracts/core/GasTankDepositor.sol similarity index 98% rename from contracts/contracts/core/GasTankManager.sol rename to contracts/contracts/core/GasTankDepositor.sol index 1d046e07c..c41e6c7b1 100644 --- a/contracts/contracts/core/GasTankManager.sol +++ b/contracts/contracts/core/GasTankDepositor.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.26; import {Errors} from "../utils/Errors.sol"; -/// @title GasTankManager +/// @title GasTankDepositor /// @notice Coordinates on-demand ETH Transfers to the RPC Service for EOA custodial gas tanks. /// @dev This contract implicitly trusts the RPC_SERVICE address. -contract GasTankManager { +contract GasTankDepositor { address public immutable RPC_SERVICE; uint256 public immutable MINIMUM_DEPOSIT; diff --git a/contracts/doc/GasTankManager.md b/contracts/doc/GasTankDepositor.md similarity index 79% rename from contracts/doc/GasTankManager.md rename to contracts/doc/GasTankDepositor.md index 9a7e7f3fb..982643367 100644 --- a/contracts/doc/GasTankManager.md +++ b/contracts/doc/GasTankDepositor.md @@ -1,8 +1,8 @@ -# GasTankManager Contract Documentation +# GasTankDepositor Contract Documentation ## Overview -The `GasTankManager` contract coordinates on-demand ETH transfers from user EOAs to an RPC service-managed EOA for custodial gas tank management. This contract enables automatic gas tank top-ups using the ERC-7702 standard, allowing users to delegate smart contract functionality to their EOA addresses without requiring a contract wallet. +The `GasTankDepositor` contract coordinates on-demand ETH transfers from user EOAs to an RPC service-managed EOA for custodial gas tank management. This contract enables automatic gas tank top-ups using the ERC-7702 standard, allowing users to delegate smart contract functionality to their EOA addresses without requiring a contract wallet. ## Purpose @@ -18,13 +18,13 @@ The contract facilitates a custodial gas tank system where: 1. **RPC Service EOA**: A single EOA address managed by the RPC service that receives all gas tank deposits 2. **Off-Chain Ledger**: The RPC service maintains a ledger tracking each user's custodial balance -3. **ERC-7702 Delegation**: Users delegate their EOA to the `GasTankManager` contract, enabling smart contract functionality on their EOA address +3. **ERC-7702 Delegation**: Users delegate their EOA to the `GasTankDepositor` contract, enabling smart contract functionality on their EOA address 4. **Minimum Deposit**: Immutable minimum amount that must be transferred in each top-up operation ### How It Works 1. **User Authorization** (One-time setup): - - User authorizes the `GasTankManager` contract using ERC-7702 + - User authorizes the `GasTankDepositor` contract using ERC-7702 - User sends a network transaction to attach the delegation - After delegation, the user's EOA can execute contract functions as if it were a smart contract diff --git a/contracts/test/core/GasTankManagerTest.sol b/contracts/test/core/GasTankDepositorTest.sol similarity index 65% rename from contracts/test/core/GasTankManagerTest.sol rename to contracts/test/core/GasTankDepositorTest.sol index d05cfbf56..99c8c95a2 100644 --- a/contracts/test/core/GasTankManagerTest.sol +++ b/contracts/test/core/GasTankDepositorTest.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import {GasTankManager} from "../../contracts/core/GasTankManager.sol"; +import {GasTankDepositor} from "../../contracts/core/GasTankDepositor.sol"; -contract GasTankManagerTest is Test { +contract GasTankDepositorTest is Test { uint256 public constant ALICE_PK = uint256(0xA11CE); uint256 public constant BOB_PK = uint256(0xB0B); uint256 public constant RPC_SERVICE_PK = uint256(0x1234567890); @@ -16,13 +16,13 @@ contract GasTankManagerTest is Test { address public bob = payable(vm.addr(BOB_PK)); address public rpcService = payable(vm.addr(RPC_SERVICE_PK)); - GasTankManager private _gasTankManagerImpl; + GasTankDepositor private _gasTankDepositorImpl; function setUp() public { vm.deal(alice, 10 ether); vm.deal(bob, 10 ether); vm.deal(rpcService, 10 ether); - _gasTankManagerImpl = new GasTankManager(rpcService, MINIMUM_DEPOSIT); + _gasTankDepositorImpl = new GasTankDepositor(rpcService, MINIMUM_DEPOSIT); } //=======================TESTS======================= @@ -31,17 +31,17 @@ contract GasTankManagerTest is Test { // Initial code is empty assertEq(alice.code.length, 0); - // Set delegation as the GasTankManager - _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); + // Set delegation as the GasTankDepositor + _signAndAttachDelegation(address(_gasTankDepositorImpl), ALICE_PK); - assertEq(alice.codehash, _delegateCodeHash(address(_gasTankManagerImpl))); + assertEq(alice.codehash, _delegateCodeHash(address(_gasTankDepositorImpl))); assertEq(alice.code.length, 23); } function testRemovesDelegationCodeAtAddress() public { - // Set delegation as the GasTankManager - _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); - assertEq(alice.codehash, _delegateCodeHash(address(_gasTankManagerImpl))); + // Set delegation as the GasTankDepositor + _signAndAttachDelegation(address(_gasTankDepositorImpl), ALICE_PK); + assertEq(alice.codehash, _delegateCodeHash(address(_gasTankDepositorImpl))); assertEq(alice.code.length, 23); // Remove delegation @@ -54,9 +54,9 @@ contract GasTankManagerTest is Test { function testFallbackRevert() public { bytes memory badData = - abi.encodeWithSelector(GasTankManager.recoverFunds.selector, address(0x55555), 1 ether, 1 ether, 1 ether); + abi.encodeWithSelector(GasTankDepositor.recoverFunds.selector, address(0x55555), 1 ether, 1 ether, 1 ether); vm.prank(alice); - (bool success,) = address(_gasTankManagerImpl).call{value: 1 ether}(badData); + (bool success,) = address(_gasTankDepositorImpl).call{value: 1 ether}(badData); assertFalse(success); } @@ -70,40 +70,40 @@ contract GasTankManagerTest is Test { } function testFundsSentDirectlyToDelegateAddress() public { - uint256 beforeBalance = address(_gasTankManagerImpl).balance; + uint256 beforeBalance = address(_gasTankDepositorImpl).balance; vm.prank(bob); - (bool success,) = address(_gasTankManagerImpl).call{value: 1 ether}(""); + (bool success,) = address(_gasTankDepositorImpl).call{value: 1 ether}(""); assertTrue(success); - uint256 afterBalance = address(_gasTankManagerImpl).balance; + uint256 afterBalance = address(_gasTankDepositorImpl).balance; assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); } function testWithdrawsFundsDirectlyFromDelegateAddress() public { - uint256 gasTankBeforeBalance = address(_gasTankManagerImpl).balance; + uint256 gasTankBeforeBalance = address(_gasTankDepositorImpl).balance; uint256 depositAmount = 1 ether; vm.prank(bob); - (bool success,) = address(_gasTankManagerImpl).call{value: depositAmount}(""); + (bool success,) = address(_gasTankDepositorImpl).call{value: depositAmount}(""); assertTrue(success); - uint256 gasTankAfterBalance = address(_gasTankManagerImpl).balance; + uint256 gasTankAfterBalance = address(_gasTankDepositorImpl).balance; assertEq(gasTankAfterBalance, gasTankBeforeBalance + depositAmount, "balance not increased"); vm.prank(rpcService); uint256 rpcServiceBeforeBalance = rpcService.balance; - _gasTankManagerImpl.recoverFunds(); + _gasTankDepositorImpl.recoverFunds(); uint256 rpcServiceAfterBalance = rpcService.balance; - assertEq(address(_gasTankManagerImpl).balance, 0, "funds not drained"); + assertEq(address(_gasTankDepositorImpl).balance, 0, "funds not drained"); assertEq(rpcServiceAfterBalance, rpcServiceBeforeBalance + depositAmount, "balance not recovered"); } function testRevertsWhenRecoverFundsIsCalledByUnknownCaller() public { vm.prank(bob); - vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotRPCService.selector, bob)); - _gasTankManagerImpl.recoverFunds(); + vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.NotRPCService.selector, bob)); + _gasTankDepositorImpl.recoverFunds(); } //=======================TESTS FOR FUNDING THE GAS TANK======================= @@ -115,7 +115,7 @@ contract GasTankManagerTest is Test { _expectGasTankFunded(rpcService, MINIMUM_DEPOSIT); vm.prank(rpcService); - GasTankManager(payable(alice)).fundGasTank(); + GasTankDepositor(payable(alice)).fundGasTank(); assertEq(rpcService.balance, rpcBalanceBefore + MINIMUM_DEPOSIT, "rpc balance not increased"); } @@ -124,8 +124,8 @@ contract GasTankManagerTest is Test { _delegate(); vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotRPCService.selector, alice)); - GasTankManager(payable(alice)).fundGasTank(); + vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.NotRPCService.selector, alice)); + GasTankDepositor(payable(alice)).fundGasTank(); } function testRpcServiceFundRevertsWhenInsufficientBalance() public { @@ -134,9 +134,9 @@ contract GasTankManagerTest is Test { vm.prank(rpcService); vm.expectRevert( - abi.encodeWithSelector(GasTankManager.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) ); - GasTankManager(payable(alice)).fundGasTank(); + GasTankDepositor(payable(alice)).fundGasTank(); } function testEOAFundsGasTank() public { @@ -147,7 +147,7 @@ contract GasTankManagerTest is Test { _expectGasTankFunded(alice, amount); vm.prank(alice); - GasTankManager(payable(alice)).fundGasTank(amount); + GasTankDepositor(payable(alice)).fundGasTank(amount); assertEq(rpcService.balance, rpcBalanceBefore + amount, "rpc balance not increased"); } @@ -158,17 +158,17 @@ contract GasTankManagerTest is Test { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(GasTankManager.MinimumDepositNotMet.selector, belowMinimumDeposit, MINIMUM_DEPOSIT) + abi.encodeWithSelector(GasTankDepositor.MinimumDepositNotMet.selector, belowMinimumDeposit, MINIMUM_DEPOSIT) ); - GasTankManager(payable(alice)).fundGasTank(belowMinimumDeposit); + GasTankDepositor(payable(alice)).fundGasTank(belowMinimumDeposit); } function testEOAFundRevertsWhenCallerNotEOA() public { _delegate(); vm.prank(rpcService); - vm.expectRevert(abi.encodeWithSelector(GasTankManager.NotThisEOA.selector, rpcService, alice)); - GasTankManager(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.NotThisEOA.selector, rpcService, alice)); + GasTankDepositor(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); } function testEOAFundRevertsWhenInsufficientBalance() public { @@ -177,20 +177,20 @@ contract GasTankManagerTest is Test { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(GasTankManager.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) ); - GasTankManager(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + GasTankDepositor(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); } //=======================HELPERS======================= function _delegate() internal { - _signAndAttachDelegation(address(_gasTankManagerImpl), ALICE_PK); + _signAndAttachDelegation(address(_gasTankDepositorImpl), ALICE_PK); } function _expectGasTankFunded(address caller, uint256 amount) internal { vm.expectEmit(true, true, true, true); - emit GasTankManager.GasTankFunded(alice, caller, amount); + emit GasTankDepositor.GasTankFunded(alice, caller, amount); } function _signAndAttachDelegation(address contractAddress, uint256 pk) internal { From 0f593c34038def133d128aceb86bbaabfd6cac6f Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Fri, 14 Nov 2025 17:36:39 -0400 Subject: [PATCH 08/13] refactor: change immutable MINIMUM_DEPOSIT to MAXIMUM_DEPOSIT --- contracts/contracts/core/GasTankDepositor.sol | 17 +++++----- contracts/doc/GasTankDepositor.md | 4 +-- contracts/test/core/GasTankDepositorTest.sol | 33 +++++++------------ 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/contracts/contracts/core/GasTankDepositor.sol b/contracts/contracts/core/GasTankDepositor.sol index c41e6c7b1..3dd98f2f3 100644 --- a/contracts/contracts/core/GasTankDepositor.sol +++ b/contracts/contracts/core/GasTankDepositor.sol @@ -8,7 +8,7 @@ import {Errors} from "../utils/Errors.sol"; /// @dev This contract implicitly trusts the RPC_SERVICE address. contract GasTankDepositor { address public immutable RPC_SERVICE; - uint256 public immutable MINIMUM_DEPOSIT; + uint256 public immutable MAXIMUM_DEPOSIT; event FundsRecovered(address indexed owner, uint256 indexed amount); event GasTankFunded(address indexed smartAccount, address indexed caller, uint256 indexed amount); @@ -21,7 +21,7 @@ contract GasTankDepositor { error NotRPCService(address caller); error InsufficientFunds(uint256 currentBalance, uint256 requiredBalance); error NotThisEOA(address msgSender, address thisAddress); - error MinimumDepositNotMet(uint256 amountToTransfer, uint256 minimumDeposit); + error MaximumDepositNotMet(uint256 amountToTransfer, uint256 maximumDeposit); modifier onlyThisEOA() { require(msg.sender == address(this), NotThisEOA(msg.sender, address(this))); @@ -33,11 +33,13 @@ contract GasTankDepositor { _; } - constructor(address rpcService, uint256 _minDeposit) { + /// @dev Writes the variables into the contract bytecode. + /// @dev No storage is used in this contract. + constructor(address rpcService, uint256 _maxDeposit) { require(rpcService != address(0), RPCServiceNotSet(rpcService)); - require(_minDeposit > 0, MinimumDepositNotMet(0, _minDeposit)); + require(_maxDeposit > 0, MaximumDepositNotMet(0, _maxDeposit)); RPC_SERVICE = rpcService; - MINIMUM_DEPOSIT = _minDeposit; + MAXIMUM_DEPOSIT = _maxDeposit; } receive() external payable { /* ETH transfers allowed. */ } @@ -60,14 +62,13 @@ contract GasTankDepositor { /// @param _amount The amount to fund the gas tank with. /// @dev Only the EOA can call this function. function fundGasTank(uint256 _amount) external onlyThisEOA { - require(_amount >= MINIMUM_DEPOSIT, MinimumDepositNotMet(_amount, MINIMUM_DEPOSIT)); _fundGasTank(_amount); } - /// @notice Transfers the minimum deposit amount of ETH from the EOA's balance to the Gas RPC Service. + /// @notice Transfers the maximum deposit amount of ETH from the EOA's balance to the Gas RPC Service. /// @dev Only the RPC Service can call this function. function fundGasTank() external onlyRPCService { - _fundGasTank(MINIMUM_DEPOSIT); + _fundGasTank(MAXIMUM_DEPOSIT); } /// @dev `fundGasTank` Internal function to fund the gas tank. diff --git a/contracts/doc/GasTankDepositor.md b/contracts/doc/GasTankDepositor.md index 982643367..e95052768 100644 --- a/contracts/doc/GasTankDepositor.md +++ b/contracts/doc/GasTankDepositor.md @@ -30,13 +30,13 @@ The contract facilitates a custodial gas tank system where: 2. **Initial Funding**: - User calls `fundGasTank(uint256 _amount)` with their desired initial deposit - - Amount must be >= `MINIMUM_DEPOSIT` + - Amount must be >= `MAXIMUM_DEPOSIT` - ETH is transferred from user's EOA to the RPC service EOA - RPC service updates off-chain ledger to reflect the deposit 3. **Automatic Top-Ups**: - When a user's off-chain ledger balance drops below threshold, RPC service calls `fundGasTank()` - - This always transfers exactly `MINIMUM_DEPOSIT` amount + - This always transfers exactly `MAXIMUM_DEPOSIT` amount - Transfer occurs directly from user's EOA balance (if sufficient funds available) - No user interaction required - fully automated - No need for `maxTransferAllowance` as RPC is restricted to minimum amount only diff --git a/contracts/test/core/GasTankDepositorTest.sol b/contracts/test/core/GasTankDepositorTest.sol index 99c8c95a2..62f0c23c1 100644 --- a/contracts/test/core/GasTankDepositorTest.sol +++ b/contracts/test/core/GasTankDepositorTest.sol @@ -8,7 +8,7 @@ contract GasTankDepositorTest is Test { uint256 public constant ALICE_PK = uint256(0xA11CE); uint256 public constant BOB_PK = uint256(0xB0B); uint256 public constant RPC_SERVICE_PK = uint256(0x1234567890); - uint256 public constant MINIMUM_DEPOSIT = 0.01 ether; + uint256 public constant MAXIMUM_DEPOSIT = 0.01 ether; address public constant ZERO_ADDRESS = address(0); bytes32 public constant EMPTY_CODEHASH = keccak256(""); @@ -22,7 +22,7 @@ contract GasTankDepositorTest is Test { vm.deal(alice, 10 ether); vm.deal(bob, 10 ether); vm.deal(rpcService, 10 ether); - _gasTankDepositorImpl = new GasTankDepositor(rpcService, MINIMUM_DEPOSIT); + _gasTankDepositorImpl = new GasTankDepositor(rpcService, MAXIMUM_DEPOSIT); } //=======================TESTS======================= @@ -108,16 +108,16 @@ contract GasTankDepositorTest is Test { //=======================TESTS FOR FUNDING THE GAS TANK======================= - function testRpcServiceFundsMinimumDeposit() public { + function testRpcServiceFundsMaximumDeposit() public { _delegate(); uint256 rpcBalanceBefore = rpcService.balance; - _expectGasTankFunded(rpcService, MINIMUM_DEPOSIT); + _expectGasTankFunded(rpcService, MAXIMUM_DEPOSIT); vm.prank(rpcService); GasTankDepositor(payable(alice)).fundGasTank(); - assertEq(rpcService.balance, rpcBalanceBefore + MINIMUM_DEPOSIT, "rpc balance not increased"); + assertEq(rpcService.balance, rpcBalanceBefore + MAXIMUM_DEPOSIT, "rpc balance not increased"); } function testRpcServiceFundRevertsWhenCallerNotRpcService() public { @@ -129,12 +129,12 @@ contract GasTankDepositorTest is Test { } function testRpcServiceFundRevertsWhenInsufficientBalance() public { - vm.deal(alice, MINIMUM_DEPOSIT - 1); + vm.deal(alice, MAXIMUM_DEPOSIT - 1); _delegate(); vm.prank(rpcService); vm.expectRevert( - abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MAXIMUM_DEPOSIT - 1, MAXIMUM_DEPOSIT) ); GasTankDepositor(payable(alice)).fundGasTank(); } @@ -152,34 +152,23 @@ contract GasTankDepositorTest is Test { assertEq(rpcService.balance, rpcBalanceBefore + amount, "rpc balance not increased"); } - function testEOAFundRevertsBelowMinimumDeposit() public { - _delegate(); - uint256 belowMinimumDeposit = MINIMUM_DEPOSIT - 1 wei; - - vm.prank(alice); - vm.expectRevert( - abi.encodeWithSelector(GasTankDepositor.MinimumDepositNotMet.selector, belowMinimumDeposit, MINIMUM_DEPOSIT) - ); - GasTankDepositor(payable(alice)).fundGasTank(belowMinimumDeposit); - } - function testEOAFundRevertsWhenCallerNotEOA() public { _delegate(); vm.prank(rpcService); vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.NotThisEOA.selector, rpcService, alice)); - GasTankDepositor(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + GasTankDepositor(payable(alice)).fundGasTank(MAXIMUM_DEPOSIT); } function testEOAFundRevertsWhenInsufficientBalance() public { - vm.deal(alice, MINIMUM_DEPOSIT - 1); + vm.deal(alice, MAXIMUM_DEPOSIT - 1); _delegate(); vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MINIMUM_DEPOSIT - 1, MINIMUM_DEPOSIT) + abi.encodeWithSelector(GasTankDepositor.InsufficientFunds.selector, MAXIMUM_DEPOSIT - 1, MAXIMUM_DEPOSIT) ); - GasTankDepositor(payable(alice)).fundGasTank(MINIMUM_DEPOSIT); + GasTankDepositor(payable(alice)).fundGasTank(MAXIMUM_DEPOSIT); } //=======================HELPERS======================= From c3758d7f0f43b367f8d8c894b53b8608ea14f881 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 18 Nov 2025 11:19:45 -0400 Subject: [PATCH 09/13] refactor: remove unused error signatures --- contracts/contracts/core/GasTankDepositor.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/contracts/core/GasTankDepositor.sol b/contracts/contracts/core/GasTankDepositor.sol index 3dd98f2f3..c1aa39017 100644 --- a/contracts/contracts/core/GasTankDepositor.sol +++ b/contracts/contracts/core/GasTankDepositor.sol @@ -14,8 +14,6 @@ contract GasTankDepositor { event GasTankFunded(address indexed smartAccount, address indexed caller, uint256 indexed amount); error FailedToRecoverFunds(address owner, uint256 amount); - error NotValidCaller(address caller); - error InvalidAmount(); error FailedToFundGasTank(address rpcProvider, uint256 transferAmount); error RPCServiceNotSet(address provider); error NotRPCService(address caller); From 7eb67ae9b1d094d4adff81637230af749ce81cd8 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 18 Nov 2025 13:57:24 -0400 Subject: [PATCH 10/13] refactor: add conditional to handle ETH transfers --- contracts/contracts/core/GasTankDepositor.sol | 21 ++---- contracts/test/core/GasTankDepositorTest.sol | 67 +++++++++---------- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/contracts/contracts/core/GasTankDepositor.sol b/contracts/contracts/core/GasTankDepositor.sol index c1aa39017..f50e92325 100644 --- a/contracts/contracts/core/GasTankDepositor.sol +++ b/contracts/contracts/core/GasTankDepositor.sol @@ -9,11 +9,10 @@ import {Errors} from "../utils/Errors.sol"; contract GasTankDepositor { address public immutable RPC_SERVICE; uint256 public immutable MAXIMUM_DEPOSIT; + address public immutable GAS_TANK_ADDRESS; - event FundsRecovered(address indexed owner, uint256 indexed amount); event GasTankFunded(address indexed smartAccount, address indexed caller, uint256 indexed amount); - error FailedToRecoverFunds(address owner, uint256 amount); error FailedToFundGasTank(address rpcProvider, uint256 transferAmount); error RPCServiceNotSet(address provider); error NotRPCService(address caller); @@ -38,24 +37,19 @@ contract GasTankDepositor { require(_maxDeposit > 0, MaximumDepositNotMet(0, _maxDeposit)); RPC_SERVICE = rpcService; MAXIMUM_DEPOSIT = _maxDeposit; + GAS_TANK_ADDRESS = address(this); } - receive() external payable { /* ETH transfers allowed. */ } + receive() external payable { + if (address(this) == GAS_TANK_ADDRESS) { + revert Errors.InvalidReceive(); + } + } fallback() external payable { revert Errors.InvalidFallback(); } - /// @notice Recovers funds inadvertently sent to this contract directly. - function recoverFunds() external onlyRPCService { - uint256 balance = address(this).balance; - - (bool success,) = RPC_SERVICE.call{value: balance}(""); - require(success, FailedToRecoverFunds(RPC_SERVICE, balance)); - - emit FundsRecovered(RPC_SERVICE, balance); - } - /// @notice Transfers ETH from the EOA's balance to the Gas RPC Service. /// @param _amount The amount to fund the gas tank with. /// @dev Only the EOA can call this function. @@ -69,7 +63,6 @@ contract GasTankDepositor { _fundGasTank(MAXIMUM_DEPOSIT); } - /// @dev `fundGasTank` Internal function to fund the gas tank. function _fundGasTank(uint256 _amountToTransfer) internal { require(address(this).balance >= _amountToTransfer, InsufficientFunds(address(this).balance, _amountToTransfer)); diff --git a/contracts/test/core/GasTankDepositorTest.sol b/contracts/test/core/GasTankDepositorTest.sol index 62f0c23c1..a5cf7ef7a 100644 --- a/contracts/test/core/GasTankDepositorTest.sol +++ b/contracts/test/core/GasTankDepositorTest.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; import {GasTankDepositor} from "../../contracts/core/GasTankDepositor.sol"; +import {Errors} from "../../contracts/utils/Errors.sol"; contract GasTankDepositorTest is Test { uint256 public constant ALICE_PK = uint256(0xA11CE); @@ -50,17 +51,11 @@ contract GasTankDepositorTest is Test { assertEq(alice.code.length, 0); } - //=======================TESTS FOR IMPROPER CALLS TO THE GAS TANK MANAGER======================= + //=======================TESTS FOR RECEIVING FUNDS======================= - function testFallbackRevert() public { - bytes memory badData = - abi.encodeWithSelector(GasTankDepositor.recoverFunds.selector, address(0x55555), 1 ether, 1 ether, 1 ether); - vm.prank(alice); - (bool success,) = address(_gasTankDepositorImpl).call{value: 1 ether}(badData); - assertFalse(success); - } + function testReceivesFunds() public { + _delegate(); - function testReceiveNoRevert() public { uint256 beforeBalance = alice.balance; vm.prank(bob); (bool success,) = address(alice).call{value: 1 ether}(""); @@ -69,43 +64,45 @@ contract GasTankDepositorTest is Test { assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); } - function testFundsSentDirectlyToDelegateAddress() public { - uint256 beforeBalance = address(_gasTankDepositorImpl).balance; + //=======================TESTS FOR RECEIVE AND FALLBACK======================= - vm.prank(bob); - (bool success,) = address(_gasTankDepositorImpl).call{value: 1 ether}(""); - assertTrue(success); + function testFallbackRevert() public { + bytes memory badData = abi.encodeWithSelector(bytes4(keccak256("invalidFunction()"))); + vm.prank(alice); + (bool success,) = address(_gasTankDepositorImpl).call{value: 1 ether}(badData); + assertFalse(success); + } - uint256 afterBalance = address(_gasTankDepositorImpl).balance; - assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); + function testFundsSentDirectlyToDelegateAddress() public { + vm.prank(bob); + (bool success, bytes memory data) = address(_gasTankDepositorImpl).call{value: 1 ether}(""); + assertFalse(success); + bytes4 selector; + assembly { + selector := mload(add(data, 0x20)) + } + assertEq(selector, Errors.InvalidReceive.selector); } - function testWithdrawsFundsDirectlyFromDelegateAddress() public { - uint256 gasTankBeforeBalance = address(_gasTankDepositorImpl).balance; - uint256 depositAmount = 1 ether; + function testFundsSentDirectlyToEOAAddressWithDelegation() public { + _delegate(); + uint256 beforeBalance = alice.balance; vm.prank(bob); - (bool success,) = address(_gasTankDepositorImpl).call{value: depositAmount}(""); + (bool success,) = alice.call{value: 1 ether}(""); assertTrue(success); - - uint256 gasTankAfterBalance = address(_gasTankDepositorImpl).balance; - assertEq(gasTankAfterBalance, gasTankBeforeBalance + depositAmount, "balance not increased"); - - vm.prank(rpcService); - uint256 rpcServiceBeforeBalance = rpcService.balance; - _gasTankDepositorImpl.recoverFunds(); - uint256 rpcServiceAfterBalance = rpcService.balance; - - assertEq(address(_gasTankDepositorImpl).balance, 0, "funds not drained"); - assertEq(rpcServiceAfterBalance, rpcServiceBeforeBalance + depositAmount, "balance not recovered"); + uint256 afterBalance = alice.balance; + assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); } - function testRevertsWhenRecoverFundsIsCalledByUnknownCaller() public { + function testFundsSentDirectlyToEOAAddressWithoutDelegation() public { + uint256 beforeBalance = alice.balance; vm.prank(bob); - vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.NotRPCService.selector, bob)); - _gasTankDepositorImpl.recoverFunds(); + (bool success,) = address(alice).call{value: 1 ether}(""); + assertTrue(success); + uint256 afterBalance = alice.balance; + assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); } - //=======================TESTS FOR FUNDING THE GAS TANK======================= function testRpcServiceFundsMaximumDeposit() public { From fdebfc8d60c2d66174a52b5643b1979238a479ab Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 18 Nov 2025 14:42:54 -0400 Subject: [PATCH 11/13] refactor: add constructor tests --- contracts/test/core/GasTankDepositorTest.sol | 31 ++++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/contracts/test/core/GasTankDepositorTest.sol b/contracts/test/core/GasTankDepositorTest.sol index a5cf7ef7a..ccaa08abc 100644 --- a/contracts/test/core/GasTankDepositorTest.sol +++ b/contracts/test/core/GasTankDepositorTest.sol @@ -26,6 +26,24 @@ contract GasTankDepositorTest is Test { _gasTankDepositorImpl = new GasTankDepositor(rpcService, MAXIMUM_DEPOSIT); } + //=======================TESTS FOR CONSTRUCTOR======================= + + function testConstructorRevertsWhenRpcServiceIsZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.RPCServiceNotSet.selector, ZERO_ADDRESS)); + new GasTankDepositor(ZERO_ADDRESS, MAXIMUM_DEPOSIT); + } + + function testConstructorRevertsWhenMaximumDepositIsZero() public { + vm.expectRevert(abi.encodeWithSelector(GasTankDepositor.MaximumDepositNotMet.selector, 0, 0)); + new GasTankDepositor(rpcService, 0); + } + + function testConstructorSetsVariables() public view { + assertEq(_gasTankDepositorImpl.RPC_SERVICE(), rpcService); + assertEq(_gasTankDepositorImpl.MAXIMUM_DEPOSIT(), MAXIMUM_DEPOSIT); + assertEq(_gasTankDepositorImpl.GAS_TANK_ADDRESS(), address(_gasTankDepositorImpl)); + } + //=======================TESTS======================= function testSetsDelegationCodeAtAddress() public { @@ -51,19 +69,6 @@ contract GasTankDepositorTest is Test { assertEq(alice.code.length, 0); } - //=======================TESTS FOR RECEIVING FUNDS======================= - - function testReceivesFunds() public { - _delegate(); - - uint256 beforeBalance = alice.balance; - vm.prank(bob); - (bool success,) = address(alice).call{value: 1 ether}(""); - assertTrue(success); - uint256 afterBalance = alice.balance; - assertEq(afterBalance, beforeBalance + 1 ether, "balance not increased"); - } - //=======================TESTS FOR RECEIVE AND FALLBACK======================= function testFallbackRevert() public { From 5819e128f5aaadfbcb35dcf19f850bf2c0fa3ea6 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 19 Nov 2025 16:13:27 -0400 Subject: [PATCH 12/13] refactor: change GASTANK_ADDRESS to DELEGATION_ADDR --- contracts/contracts/core/GasTankDepositor.sol | 6 +++--- contracts/test/core/GasTankDepositorTest.sol | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/core/GasTankDepositor.sol b/contracts/contracts/core/GasTankDepositor.sol index f50e92325..e9ebff241 100644 --- a/contracts/contracts/core/GasTankDepositor.sol +++ b/contracts/contracts/core/GasTankDepositor.sol @@ -9,7 +9,7 @@ import {Errors} from "../utils/Errors.sol"; contract GasTankDepositor { address public immutable RPC_SERVICE; uint256 public immutable MAXIMUM_DEPOSIT; - address public immutable GAS_TANK_ADDRESS; + address public immutable DELEGATION_ADDR; event GasTankFunded(address indexed smartAccount, address indexed caller, uint256 indexed amount); @@ -37,11 +37,11 @@ contract GasTankDepositor { require(_maxDeposit > 0, MaximumDepositNotMet(0, _maxDeposit)); RPC_SERVICE = rpcService; MAXIMUM_DEPOSIT = _maxDeposit; - GAS_TANK_ADDRESS = address(this); + DELEGATION_ADDR = address(this); } receive() external payable { - if (address(this) == GAS_TANK_ADDRESS) { + if (address(this) == DELEGATION_ADDR) { revert Errors.InvalidReceive(); } } diff --git a/contracts/test/core/GasTankDepositorTest.sol b/contracts/test/core/GasTankDepositorTest.sol index ccaa08abc..e23964079 100644 --- a/contracts/test/core/GasTankDepositorTest.sol +++ b/contracts/test/core/GasTankDepositorTest.sol @@ -41,7 +41,7 @@ contract GasTankDepositorTest is Test { function testConstructorSetsVariables() public view { assertEq(_gasTankDepositorImpl.RPC_SERVICE(), rpcService); assertEq(_gasTankDepositorImpl.MAXIMUM_DEPOSIT(), MAXIMUM_DEPOSIT); - assertEq(_gasTankDepositorImpl.GAS_TANK_ADDRESS(), address(_gasTankDepositorImpl)); + assertEq(_gasTankDepositorImpl.DELEGATION_ADDR(), address(_gasTankDepositorImpl)); } //=======================TESTS======================= From 28f1fbf9d57cacb6d5d1fa1ea457511a0d8d5982 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 19 Nov 2025 16:17:06 -0400 Subject: [PATCH 13/13] refactor: updated the docs --- contracts/doc/GasTankDepositor.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/doc/GasTankDepositor.md b/contracts/doc/GasTankDepositor.md index e95052768..6425d0e03 100644 --- a/contracts/doc/GasTankDepositor.md +++ b/contracts/doc/GasTankDepositor.md @@ -24,12 +24,13 @@ The contract facilitates a custodial gas tank system where: ### How It Works 1. **User Authorization** (One-time setup): + - User adds FastRPC network to their wallet. - User authorizes the `GasTankDepositor` contract using ERC-7702 - User sends a network transaction to attach the delegation - After delegation, the user's EOA can execute contract functions as if it were a smart contract 2. **Initial Funding**: - - User calls `fundGasTank(uint256 _amount)` with their desired initial deposit + - User just needs to perform a transaction once the FastRPC network is added to their wallet and EOA has the `MAXIMUM_DEPOSIT` - Amount must be >= `MAXIMUM_DEPOSIT` - ETH is transferred from user's EOA to the RPC service EOA - RPC service updates off-chain ledger to reflect the deposit @@ -39,7 +40,6 @@ The contract facilitates a custodial gas tank system where: - This always transfers exactly `MAXIMUM_DEPOSIT` amount - Transfer occurs directly from user's EOA balance (if sufficient funds available) - No user interaction required - fully automated - - No need for `maxTransferAllowance` as RPC is restricted to minimum amount only 4. **Off-Chain Ledger Operations**: - RPC service tracks user balances in off-chain ledger