fix: KEEP-394 clamp Pimlico fees to EIP-1559 invariant#1093
Merged
Conversation
suisuss
added a commit
that referenced
this pull request
May 3, 2026
Three review follow-ups for PR #1093: 1. Extract clampSponsoredFees to lib/web3/sponsored-fee-clamp.ts so the helper can be unit-tested without mocking 16 modules. The unit test now imports directly with zero mocks. 2. Replace tests/unit/sponsored-client.test.ts with an integration smoke test that exercises createSponsoredClient end-to-end: captures the args passed to createSmartAccountClient, pulls the estimateFeesPerGas callback back out, and asserts the clamp is actually invoked. Guards against accidental regressions where the call to clampSponsoredFees is removed. 3. Tighten the helper's docstring: drop the attribution to "Pimlico's priority floor" (which conflated Pimlico's behavior with our chain config). Describe the failure mode in terms of base-fee dynamics only. Adds two boundary cases to the helper test (off-by-one above/below). Net coverage: 9 tests across 2 files, mock count drops from 16 in the unit test path to 0.
e83ebb5 to
39d4b5c
Compare
39d4b5c to
44f2321
Compare
Pimlico's getUserOperationGasPrice().fast has been observed on Sepolia returning a pair where maxPriorityFeePerGas > maxFeePerGas, which violates the EIP-1559 invariant required by ERC-4337 bundlers. Add clampSponsoredFees() (pure helper, exported for unit testing) that lifts maxFeePerGas to maxPriorityFeePerGas when the invariant is violated, and apply it at sponsored-client construction. The on-chain cost paid is still baseFee + tip, so the only effect is that the cap moves up; spend is unchanged in normal conditions. Mirrors the KEEP-384 fix in lib/web3/gas-strategy.ts that addressed the same class of bug for the direct-signing path. Tests cover the helper directly (invalid pair, valid pair, equality, zero, and the wei boundary cases) plus a smoke test that asserts the clamp is wired through createSponsoredClient's estimateFeesPerGas.
44f2321 to
63d5034
Compare
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
ℹ️ No PR Environment to Clean UpNo PR environment was found for this PR. This is expected if:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pimlico's
getUserOperationGasPrice().fastwas observed on Sepolia returning a pair wheremaxPriorityFeePerGas > maxFeePerGas, violating the EIP-1559 invariant required by ERC-4337 bundlers.lib/web3/sponsored-client.tswas passing the result ofgetUserOperationGasPrice().fastdirectly intoestimateFeesPerGaswithout enforcing the invariant. This PR addsclampSponsoredFees(pure helper, exported for unit testing) that liftsmaxFeePerGastomaxPriorityFeePerGaswhen violated, and wires it through sponsored-client construction.The on-chain cost paid is still
baseFee + tip, so the only effect of the clamp is that the cap moves up; spend is unchanged in normal conditions.Mirrors the KEEP-384 fix in
lib/web3/gas-strategy.tsthat addressed the same class of bug for the direct-signing path.Linear
KEEP-394
Environments
NEXT_PUBLIC_GAS_SPONSORSHIP_ENABLED=true- was affected, this PR fixesNEXT_PUBLIC_GAS_SPONSORSHIP_ENABLED=false- sponsored path is gated off, no behavioral changeTest plan
pnpm vitest run tests/unit/sponsored-fee-clamp.test.ts- 6/6 pass (invalid pair lifted, valid preserved, equal preserved, zero pair valid, both wei boundary cases)pnpm vitest run tests/unit/sponsored-client.test.ts- 2/2 pass (clamp is wired throughcreateSponsoredClient.estimateFeesPerGas; valid pair passes through)pnpm check- clean on changed filespnpm type-check- cleanInvestigation notes
floor-max-priority-fee-per-gasCLI option (src/cli/config/options.ts) that usesmaxBigInt(floor, priority)to enforce a priority minimum (src/handlers/gasPriceManager.ts:138-148). The 100,000,000 wei value observed in production matches0.1 gweiexactly, consistent with such a floor being configured on Pimlico's hosted bundler, though we have not confirmed the deployed config.bumpTheGasPrice(gasPriceManager.ts:153:maxFeePerGas: maxBigInt(maxFeePerGas, maxPriorityFeePerGas)). The production observation either pre-dates that line or hits a path that bypasses it. Either way, defensive clamping on our side is required because we do not control Pimlico's deployed version.2,463,760 weimaxFeePerGasfigure that has appeared in incident logs is Pimlico's buffered value, not the chain's raw base fee.