Skip to content

base-mcp: send_calls userOp gas estimation reverts for gas-heavy partner contract calls (eth_call succeeds) #51

@velinussage

Description

@velinussage

Summary

send_calls userOp gas estimation reverts for a partner contract call that simulates successfully via eth_call. The same wallet, same RPC, identical calldata succeeds in simulation when no explicit gas is set, but is rejected at send_calls time with failed to estimate gas for user operation: useroperation reverted: execution reverted. There is no gas parameter on the send_calls tool that would let the caller hint a higher inner-call gas budget, so any plugin whose calls exceed the (apparently) internal cap is silently blocked.

This affects the base-mcp skill custom-plugin path documented at references/custom-plugins.md (and docs.base.org/ai-agents/plugins/custom-plugins).

Environment

  • MCP server: https://mcp.base.org (live, current as of 2026-05-27)
  • Chain: Base mainnet (chain id 8453)
  • Wallet under test (Smart Wallet): 0x93d9fcf2f6b2b1abe4bd55d8e21e87d8d4e48a1f
  • Target contract: BoonV3 at 0x22aC2E603D4B1CaAb3A8433f1691BA6158A896AF (verified on Basescan, ~31.7 KB code, not paused)
  • USDC: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
  • Plugin: a custom Base MCP plugin (Boon Protocol — USDC tipping) following the documented "fetch calldata via web_request, submit via send_calls" pattern. Plugin spec: https://docs.boonprotocol.com/base-mcp-plugin/boon (private repo source).

Reproduction

1. Single-call send_calls — USDC approve(boonV3, 100000) (0.10 USDC allowance)

  • Result: ✅ Works. send_calls returns an approvalUrl, user approves in Base Account, tx mines.
  • Tx hash: 0xc2e346ca220255225fcfb38e2b0d1c1ca83b245d03f151be6228251de083616c
  • Block: 46552554
  • gasUsed: 597,375 (includes Smart Wallet first-deploy overhead)

2. Single-call send_callsBoonV3.tip(...) (selector 0x4797ac29), tip 0.10 USDC to github:vbuterin

ABI: tip(bytes32 handleHash, string handle, address token, uint256 amount, string note, bool isPublic, (uint256 deadline, uint8 v, bytes32 r, bytes32 s) permit)

  • Result: ❌ Transaction validation failed: failed to estimate gas for user operation: useroperation reverted: execution reverted. Verify the calls and try again.
  • Failure happens before any approvalUrl is returned — i.e. during userOp gas estimation, not at user-signing time.

3. Direct eth_call with the exact same calldata, same from, no explicit gas field

  • Result: ✅ Returns 0x...0000003 (tipId = 3). Function executes successfully in simulation.
  • cast call --from 0x93d9...a1f 0x22aC...96AF 'tip(bytes32,string,address,uint256,string,bool,(uint256,uint8,bytes32,bytes32))' ...0x...0000003.

4. Same eth_call, but with "gas":"0x186a00" (1,600,000) explicitly set

  • Result: ❌ execution reverted with no error string — consistent with gas exhaustion (not a contract require failure, which would surface a revert reason).

5. Calldata sanity checks

  • Calldata is byte-identical to what cast calldata produces for the canonical ABI (diff confirmed).
  • Selector 0x4797ac29 matches the contract's documented tip(...) signature.
  • handleHash = keccak256("github:vbuterin") verified on-chain.
  • Allowance and balance on-chain are sufficient: 0.10 USDC allowance, 2.5 USDC balance, 0.0006 ETH for gas.

Diagnosis

send_calls's userOp gas estimator appears to apply an internal gas cap on inner calls that is lower than the actual gas required for state-write-heavy contract calls (~200k+ for the tip flow due to escrow state writes + USDC transferFrom + event emission). The cheap approve call (~25k gas) passes the cap; the tip call does not, even though it is well within reasonable block-gas-limit territory and eth_call confirms it executes correctly under the default block-gas cap.

The "1,600,000 gas → revert with no string" result in step 4 is the same failure mode as send_calls, which is consistent with the estimator running the call under a fixed cap below what the contract needs and then surfacing the cap-exhausted revert as a generic execution reverted.

Expected behavior

Any call that simulates successfully via eth_call with default (block-gas-limit) gas should be submittable via send_calls. Either:

  • raise the internal inner-call gas cap to the userOp's actual callGasLimit (and let the bundler/paymaster handle the real bound), or
  • expose an explicit per-call gas field on the send_calls tool so the caller can hint a higher budget when they have already simulated the call.

Impact

This silently blocks every partner plugin whose contract calls exceed the internal cap — likely most non-trivial state-mutating calls. The Boon tip flow is a fairly typical "escrow update + event emission + transferFrom" pattern; we should not be hitting a gas cap there.

Today the only escape hatch for users who hit this is to abandon Base MCP for that call and submit the transaction directly through the Base Account web UI, which defeats the point of the custom-plugins path.

Asks

  1. Confirm whether send_calls applies an internal gas cap on inner calls, and if so, document it.
  2. Either lift the cap to the userOp's callGasLimit or add an optional gas field to send_calls calls.
  3. Make the error message distinguish "inner-call gas cap exhausted" from "real contract revert" so plugin authors can diagnose this without an out-of-band eth_call.

Happy to provide raw calldata, RPC traces, or run additional repro steps. Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions