⚙️ 1. Current Situation
The ChatterPay.sol contract holds the following dependencies as constructor-set storage variables:
entryPoint
paymaster
swapRouter
chatterPayAdmin (the wallet admin — not the user or wallet owner)
These values are passed during the proxy initialization from the ChatterPayWalletFactory. Once deployed, they cannot be changed per wallet.
Even though factory.owner() is used to resolve chatterPayAdmin dynamically, the others remain fixed.
This limits flexibility in several ways:
- If the factory updates
paymaster, entryPoint, or swapRouter, existing wallets won’t benefit.
- Selective overrides (e.g., only some wallets using a new paymaster) are not possible.
- It prevents clean contract upgrades for gas efficiency or bug fixes.
💡 2. Strategy: Add Local Override with Fallback to Factory
We propose to add optional local overrides, allowing each wallet to:
- Use the
factory.X() default (just like it does with factory.owner())
- Or opt-in to a local override (per wallet)
This requires:
- For each value (
entryPoint, paymaster, swapRouter, chatterPayAdmin):
- A
bool useFactoryX flag
- A local override storage slot (
localX)
- A getter that resolves to
localX if override is enabled; otherwise calls factory
This allows:
- Default behavior to follow centralized updates from the factory.
- Optional per-wallet overrides for custom needs (e.g., different routers or paymasters per region, EOA recovery migration, testing purposes, etc).
The flags will be settable via a grouped setter (setFactoryFallbackConfig) restricted to the wallet owner(), and used to dynamically determine which source to reference.
⛽️ 3. Gas Analysis With Fallback Flag
🔬 Read Cost Breakdown
-
✅ If useFactoryX == false:
- Reads local value →
SLOAD → ~100 gas.
-
✅ If useFactoryX == true:
- Calls
factory.X() → CALL → ~700 gas.
-
➕ Additional cost:
- Checking the flag (
SLOAD) and branching → negligible.
➡️ Conclusion: In worst case, this adds ~800 gas overhead per access — acceptable for wallet calls, especially considering the flexibility benefit.
📊 4. Strategy Comparison
| Strategy |
Flexibility |
Gas Cost |
Upgradeable? |
Per-Wallet Config |
| Hardcoded values (current) |
❌ Low |
✅ Low |
❌ No |
❌ No |
| Always call factory |
⚠️ Medium |
❌ High |
✅ Yes |
❌ No |
| Local override + fallback (proposed) |
✅ High |
⚠️ Medium |
✅ Yes |
✅ Yes |
✅ 5. Recommended Implementation (Hybrid Fallback)
We recommend the Hybrid Fallback approach.
For each dependency (paymaster, entryPoint, router, chatterPayAdmin):
- Add a
bool useFactoryX
- Add a
localX address
- Add a
getter() function
- Include setters for both the value and the flag
🧩 Getter Snippets
function getPaymaster() public view returns (address) {
return useFactoryPaymaster ? factory.paymaster() : localPaymaster;
}
function getEntryPoint() public view returns (address) {
return useFactoryEntryPoint ? factory.entryPoint() : localEntryPoint;
}
function getRouter() public view returns (address) {
return useFactoryRouter ? factory.router() : localRouter;
}
function getChatterPayAdmin() public view returns (address) {
return useFactoryChatterPayAdmin ? factory.owner() : localChatterPayAdmin;
}
✍️ Setter Snippets
// Setters for fallback usage
function setUseFactoryContracts(
bool _useFactoryEntryPoint,
bool _useFactoryPaymaster,
bool _useFactoryRouter,
bool _useFactoryChatterPayAdmin
) external onlyChatterPayAdmin {
useFactoryEntryPoint = _useFactoryEntryPoint;
useFactoryPaymaster = _useFactoryPaymaster;
useFactoryRouter = _useFactoryRouter;
useFactoryChatterPayAdmin = _useFactoryChatterPayAdmin;
}
// Setters for local override values
function setLocalPaymaster(address _paymaster) external onlyChatterPayAdmin {
if (_paymaster == address(0)) revert ChatterPay__InvalidAddress();
localPaymaster = _paymaster;
}
function setLocalEntryPoint(address _entryPoint) external onlyChatterPayAdmin {
if (_entryPoint == address(0)) revert ChatterPay__InvalidAddress();
localEntryPoint = _entryPoint;
}
function setLocalRouter(address _router) external onlyChatterPayAdmin {
if (_router == address(0)) revert ChatterPay__InvalidAddress();
localRouter = _router;
}
function setLocalChatterPayAdmin(address _admin) external onlyChatterPayAdmin {
if (_admin == address(0)) revert ChatterPay__InvalidAddress();
localChatterPayAdmin = _admin;
}
🔒 Modifiers
modifier onlyChatterPayAdmin() {
require(msg.sender == getChatterPayAdmin(), "Not chatterPay admin");
_;
}
🛡️ Final Recommendations
- Use
factory.owner() as the canonical ChatterPay admin — avoid confusion with wallet.owner() (user).
- Consider locking setter functions after initial configuration using a
permanentLock() toggle or renounceUpgradeRights().
- Expose a public
getConfig() function to improve UI visibility of wallet state.
- Emit
events on every update to enhance traceability and auditability.
📌 Auditor Notes
- Ensure fallback logic is read-only, with no external calls in sensitive paths (e.g.,
validateUserOp).
- Keep flag-based fallback strictly under
onlyOwner control.
- Carefully distinguish between:
owner() → the user (wallet EOA)
getChatterPayOwner() → the factory admin (factory.owner() by default)
- Consider validating
factory contract behavior (e.g., interface conformance) if overridden contracts are expected to change drastically.
🛠 Optional Extensions
- Add individual setters for each config (
overridePaymaster, overrideEntryPoint, etc.) for clearer UX/API.
- Allow factory to lock override ability for certain wallets (if needed for compliance or safety).
🧠 Additional Considerations Based on Factory Logic
Given that the ChatterPayWalletFactory already manages global configuration for entryPoint, paymaster, and router, and has an upgradeable walletImplementation, the fallback approach is consistent with the existing architecture.
Additional recommendations:
- ✅ Centralized Upgrade Flow: Changing
paymaster, entryPoint, or router in the factory automatically affects wallets that use factory fallbacks.
- 🚫 Immutable Fields: Wallets that store those fields at initialization time cannot benefit from future updates — requiring upgrades or redeployment.
- 🔐 Field-level Lock Option: Add a
lockedFallbackConfig mapping in the factory to prevent certain wallets from opting out of factory defaults — useful in managed or restricted environments.
- 📦 Wallet Metadata: Consider emitting an event on wallet creation with its fallback config and override values, for easy off-chain indexing.
This approach provides a clean balance between upgradeability and per-wallet autonomy, while leveraging the factory as the single source of truth.
⚙️ 1. Current Situation
The
ChatterPay.solcontract holds the following dependencies as constructor-set storage variables:entryPointpaymasterswapRouterchatterPayAdmin(the wallet admin — not the user or wallet owner)These values are passed during the proxy initialization from the
ChatterPayWalletFactory. Once deployed, they cannot be changed per wallet.Even though
factory.owner()is used to resolvechatterPayAdmindynamically, the others remain fixed.This limits flexibility in several ways:
paymaster,entryPoint, orswapRouter, existing wallets won’t benefit.💡 2. Strategy: Add Local Override with Fallback to Factory
We propose to add optional local overrides, allowing each wallet to:
factory.X()default (just like it does withfactory.owner())This requires:
entryPoint,paymaster,swapRouter,chatterPayAdmin):bool useFactoryXflaglocalX)localXif override is enabled; otherwise calls factoryThis allows:
The flags will be settable via a grouped setter (
setFactoryFallbackConfig) restricted to the walletowner(), and used to dynamically determine which source to reference.⛽️ 3. Gas Analysis With Fallback Flag
🔬 Read Cost Breakdown
✅ If
useFactoryX == false:SLOAD→ ~100 gas.✅ If
useFactoryX == true:factory.X()→CALL→ ~700 gas.➕ Additional cost:
SLOAD) and branching → negligible.➡️ Conclusion: In worst case, this adds ~800 gas overhead per access — acceptable for wallet calls, especially considering the flexibility benefit.
📊 4. Strategy Comparison
✅ 5. Recommended Implementation (Hybrid Fallback)
We recommend the Hybrid Fallback approach.
For each dependency (
paymaster,entryPoint,router,chatterPayAdmin):bool useFactoryXlocalXaddressgetter()function🧩 Getter Snippets
✍️ Setter Snippets
🔒 Modifiers
🛡️ Final Recommendations
factory.owner()as the canonical ChatterPay admin — avoid confusion withwallet.owner()(user).permanentLock()toggle orrenounceUpgradeRights().getConfig()function to improve UI visibility of wallet state.events on every update to enhance traceability and auditability.📌 Auditor Notes
validateUserOp).onlyOwnercontrol.owner()→ the user (wallet EOA)getChatterPayOwner()→ the factory admin (factory.owner()by default)factorycontract behavior (e.g., interface conformance) if overridden contracts are expected to change drastically.🛠 Optional Extensions
overridePaymaster,overrideEntryPoint, etc.) for clearer UX/API.🧠 Additional Considerations Based on Factory Logic
Given that the
ChatterPayWalletFactoryalready manages global configuration forentryPoint,paymaster, androuter, and has an upgradeablewalletImplementation, the fallback approach is consistent with the existing architecture.Additional recommendations:
paymaster,entryPoint, orrouterin the factory automatically affects wallets that use factory fallbacks.lockedFallbackConfigmapping in the factory to prevent certain wallets from opting out of factory defaults — useful in managed or restricted environments.This approach provides a clean balance between upgradeability and per-wallet autonomy, while leveraging the factory as the single source of truth.