Skip to content

eth: Update gasses and debug bundler failures.#3525

Open
JoeGruffins wants to merge 11 commits intodecred:masterfrom
JoeGruffins:updatev1contracts
Open

eth: Update gasses and debug bundler failures.#3525
JoeGruffins wants to merge 11 commits intodecred:masterfrom
JoeGruffins:updatev1contracts

Conversation

@JoeGruffins
Copy link
Member

@JoeGruffins JoeGruffins commented Feb 20, 2026

closes #3316

The contracts here are pointing at the current v1 contract on chain. However, there are problems:

2026-02-20 22:06:21.008 [ERR] CORE[polygon]: bundler gas estimation failed, using precalculated values: User operation must include a paymaster for sponsorship. For more information, see: https://www.alchemy.com/docs/wallets/resources/chain-reference/polygon-pos#transactions. Contact support@alchemy.com with any questions.
2026-02-20 22:06:28.096 [ERR] CORE[base]: bundler gas estimation failed, using precalculated values: Invalid user operation for entry point: 0x0000000071727de22e5e9d8baf0edac6f37da032
2026-02-20 22:10:12.742 [ERR] CORE[eth]: bundler gas estimation failed, using precalculated values: Invalid user operation for entry point: 0x0000000071727de22e5e9d8baf0edac6f37da032

More context for polygon:
Alchemy requires their Gas Manager (paymaster) for all user operations on Polygon mainnet as of January 30, 2026. This is a hard requirement - you can't send unsponsored UserOps through Alchemy's bundler on Polygon anymore.

Their reasoning is that Polygon lacks a reliable private mempool, so they force all UserOps through their Gas Manager for front-running protection.

This means for gasless redeems on Polygon via Alchemy, you'd need to either:

  1. Integrate Alchemy's Gas Manager/paymaster into the UserOp
  2. Use a different bundler provider that supports unsponsored UserOps on Polygon
  3. Skip gasless redeems on Polygon entirely

@JoeGruffins
Copy link
Member Author

Eth and base are working now with alchemy. pol needs something but may just disable for now and make an issue. All gasses are updated. For tokens I only tested USDT then used those values for everything else, which should be fine... txids are included for every contract function.

@JoeGruffins JoeGruffins force-pushed the updatev1contracts branch 2 times, most recently from 4ee2496 to 3c9d466 Compare February 21, 2026 02:20
@JoeGruffins JoeGruffins force-pushed the updatev1contracts branch 2 times, most recently from e11ae2a to 66fd0ad Compare February 23, 2026 02:42
@JoeGruffins
Copy link
Member Author

JoeGruffins commented Feb 23, 2026

oh man, here when I thought we were so close.

026-02-23 08:36:27.553 [ERR] CORE[eth]: gasless test: AA redeem send error for 4 redeems: account uses banned opcode: NUMBER

The NUMBER opcode (block.number) is banned during validateUserOp execution. Our contract uses block.number in three places within validateUserOp:

// 1. Double-validation prevention
if (validatedAt[key] == block.number) { return SIG_VALIDATION_FAILED; }

// 2. Swap confirmation check
if (blockNum >= block.number ...) { return SIG_VALIDATION_FAILED; }

// 3. Record validation block
validatedAt[key] = block.number;

@JoeGruffins JoeGruffins force-pushed the updatev1contracts branch 2 times, most recently from fae3b06 to 7989608 Compare February 23, 2026 09:43
@JoeGruffins
Copy link
Member Author

Working on the contract again so all of the addresses and gasses need to be redone.

@JoeGruffins JoeGruffins force-pushed the updatev1contracts branch 3 times, most recently from c88cc39 to 7ca9c67 Compare February 24, 2026 07:13
@JoeGruffins
Copy link
Member Author

I have high hopes these values are good now. Polygon is possible for free with the proper settings, but it requires some setup on two different websites

Comment on lines +473 to +477
// Pay prefund before callGasLimit and signature checks. During gas
// estimation, the bundler sends zero gas limits and a dummy signature,
// both of which would fail checks below. The EntryPoint requires the
// prefund to be paid regardless of validation result (AA21 otherwise).
_payPrefund(missingAccountFunds);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if the prefund is paid even though the validation fails, the bundler still allows the user op to proceed? This doesn't make sense, because if the validation fails, the bundler wouldn't get the funds sent in _payPrefund.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ordering isn't for real execution. It's for gas estimation. When a bundler calls simulateValidation to estimate gas costs, it sends:

  • Zero gas limits (so the callGasLimit check at line 481 would fail)
  • A dummy signature (so the ECDSA check at line 485 would fail)

The EntryPoint's gas estimation needs to see the prefund transfer to calculate how much gas the account will pay. If _payPrefund were placed after those checks, it would never execute during estimation, and the EntryPoint would revert with AA21 (insufficient deposit).

So the flow is:

  • Gas estimation: prefund paid, then validation "fails" on dummy values — that's expected; the EntryPoint got what it needed to estimate costs.
  • Real execution: prefund paid, callGasLimit passes, real signature passes — returns SUCCESS.
  • Real execution, bad signature: prefund paid, then validation fails — EntryPoint reverts everything including the prefund. No harm done.

// execution. If execution fails, the flag remains set, preventing
// future AA redeems for that swap (the regular redeem function can
// still be used as a fallback).
mapping(bytes32 => bool) internal pendingValidation;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't prevent any attack vector, only a wallet error where the wallet signs two user ops with different nonces both redeeming the same swap.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attack it prevents relies on the two-phase execution model of ERC-4337 bundles.

How EntryPoint processes a bundle

The EntryPoint processes UserOps in two distinct phases:

  1. Validation phase — calls validateUserOp for every UserOp in the bundle. Each call pays a prefund (gas deposit) from the contract's ETH balance to the EntryPoint.
  2. Execution phase — calls redeemAA for every UserOp that passed validation.

Critically, all validations complete before any executions begin.

The attack without pendingValidation

Suppose the contract holds 10 ETH across multiple users' swaps, and an attacker has one swap worth 1 ETH.

  1. Attacker submits a bundle with N identical UserOps, all redeeming the same 1 ETH swap.
  2. Validation phase: Each validateUserOp call checks swaps[key] — the swap is still in the Filled state because no execution has happened yet. All N pass validation. Each pays a prefund (say 0.05 ETH) from the contract's pooled balance to the EntryPoint. That's N * 0.05 ETH extracted from the contract.
  3. Execution phase: The first redeemAA succeeds and marks the swap redeemed. The remaining N-1 calls revert on the "already redeemed" check. But the prefunds are not refunded — the EntryPoint keeps them (they go to the bundler as gas payment).

Net result: The contract paid N * 0.05 ETH in prefunds but only 1 swap worth 0.05 ETH in fees was legitimate. The excess came from other users' deposits. Repeat this and you drain the contract into the EntryPoint/bundler.

How pendingValidation stops it

// Line 446-448: in validateUserOp
if (pendingValidation[key]) {
return SIG_VALIDATION_FAILED;
}
// Line 460: after all checks pass
pendingValidation[key] = true;

The first UserOp's validateUserOp sets pendingValidation[key] = true. The second UserOp's validateUserOp sees the flag and returns SIG_VALIDATION_FAILED — no prefund is paid for it. Only one prefund per swap per bundle.

Then in redeemAA (line 536), the flag is cleared:

delete pendingValidation[key];

This re-enables the swap for future AA redemption attempts if needed (though typically a swap is only redeemed once).

In short

The contract holds pooled funds from many users. The prefund is paid from that pool. Without pendingValidation, an attacker can force the contract to pay arbitrarily many prefunds for a single swap by stuffing duplicate UserOps into one bundle, siphoning other users' deposits to the EntryPoint.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

@JoeGruffins
Copy link
Member Author

no idea how I fatfingered this. didn't mean to close

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Feb 25, 2026

I regret this pr is getting big but fixing some things as I go. Have tested the gasless redeem full path with a server on base testnet. Still want to look into if we really need to set the paymaster when using polygon on mainnet. The contract reorder may have made it not necessary I dont know though.

Would like to get these changes in and we can test more edge cases later.

@JoeGruffins JoeGruffins force-pushed the updatev1contracts branch 3 times, most recently from a885164 to 6361dbd Compare February 25, 2026 06:39
@JoeGruffins
Copy link
Member Author

about the bundler or paymaster being set. seems we can really know if the coin needs a paymaster. We know that polygon with alchemy needs something. The current check:

Caller level (gas test line 745, ReserveNRedemptions line 5588):

  1. No bundler at all (w.bundler == nil)
  • Gas test: "Skipped (no bundler configured)"
  • Order placement: canRedeemWithBundler returns false → ErrInsufficientRedeemFunds → "insufficient redeem funds, configure a bundler to redeem"

Inside canRedeemWithBundler (order placement only):
2. Bundler getGasPrice fails → error, order rejected
3. Lot size too small to cover gas → ErrBundlerRedemptionLotSizeTooSmall → order rejected

Inside checkBundler (both gas test and order placement):
4. Polygon + no explicit bundler (auto-detected from provider) → "a dedicated bundler endpoint is required for gasless redemptions on Polygon"
5. Bundler getGasPrice fails → "bundler gas price check failed: ..."
6. No paymaster configured → pass (nil check, return nil)
7. No v1 contract → pass (return nil)
8. Paymaster configured but broken → "paymaster check failed: ..."
9. Everything OK → pass

So the complete decision tree:

bundler nil?
  YES → blocked ("no bundler" / "insufficient redeem funds")
  NO → Polygon + bundler not explicitly set?
    YES → blocked ("dedicated bundler endpoint required")
    NO → bundler reachable? (getGasPrice)
      NO → blocked ("bundler gas price check failed")
      YES → lot size covers gas? (order placement only)
        NO → blocked ("lot size too small")
        YES → paymaster configured?
          NO → pass ✓
          YES → paymaster valid? (getPaymasterStubData)
            NO → blocked ("paymaster check failed")
            YES → pass ✓

@JoeGruffins
Copy link
Member Author

Applied suggestions from @martonp to determine if the bundler can handle redemption.

The userOp struct was using v0.6 field names (initCode,
paymasterAndData) but being sent to the v0.7 EntryPoint, causing
bundlers to reject the operations. Update to the v0.7 unpacked RPC
format (factory/factoryData, paymaster/paymasterData, etc.) and update
the hash computation to use packed accountGasLimits and gasFees fields.

Add optional paymaster support following the ERC-7677 standard so
bundler providers that require sponsored user operations (e.g. Alchemy
on Polygon) work automatically. The bundler auto-detects from the RPC
provider endpoint. For the paymaster, users set the config to their
provider's policy ID (e.g. Alchemy Gas Manager policy ID) or a custom
paymaster endpoint URL. The policy ID is passed as the context object
in pm_getPaymasterStubData and pm_getPaymasterData calls.
JoeGruffins and others added 9 commits February 27, 2026 03:39
Add checkBundler method that validates bundler connectivity (via
getGasPrice) and paymaster configuration (via getPaymasterStubData
with a dummy user op) before attempting a gasless redeem. This catches
bad endpoints, invalid policy IDs, and other misconfigurations early
with a clear error instead of failing deep in the user op flow.
Use hexutil.Big instead of big.Int for the l1Fee receipt field so
hex strings like "0x431c8e80e" returned by OP Stack RPCs are decoded
correctly.
The bundler may submit user operations via the v0.7 EntryPoint's
innerHandleOp function (selector 0x765e827f) rather than handleOps.
Add ParseInnerHandleOpData to parse the innerHandleOp calldata and
fall back to it when ParseHandleOpsData fails in both the server
(userOpBaseCoin) and client (swapOrRedemptionFeesPaidUserOp).
The sendAddr input handler was using this.selectedWalletID (the
blockchain parent's assetID) for address validation. For L2 wallets
like Base, this resolved to Ethereum mainnet (BipID 60) instead of
Base (BipID 8453), causing "no wallet found" errors when only the
L2 wallet was configured. Read the assetID from the send form's
dataset, consistent with the stepSend and sendAmt handlers.
For EVM assets, the secret can be read directly from on-chain contract
state without parsing transactions. Reduce the spent-ago threshold from
10 minutes to 30 seconds so the taker's findRedemption fallback kicks
in quickly if the server fails to relay the maker's redemption.
When a GaslessRedeem UserOp is used, the redeem notification to the
server was never sent because submitted=false deferred the notification,
but the deferred send was never implemented. Send the notification when
ConfirmRedemption reports the real transaction hash after the bundler
submits.
@JoeGruffins
Copy link
Member Author

@martonp I like your first approach better, and it seems to work fine. Was there a problem with it? Removed skandha, was it not working?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contracts: Update evm contracts before release.

2 participants