Skip to content

ChatterPay.sol: Flexibility & Upgrade Path Analysis #174

Description

@dappsar

⚙️ 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

  1. ✅ If useFactoryX == false:

    • Reads local value → SLOAD → ~100 gas.
  2. ✅ If useFactoryX == true:

    • Calls factory.X()CALL → ~700 gas.
  3. ➕ 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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request
No fields configured for Feature.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions