Skip to content

Commit 6fc10c3

Browse files
authored
[simplex,sdk]: add Circle Paymaster (#757)
1 parent c281814 commit 6fc10c3

File tree

20 files changed

+545
-177
lines changed

20 files changed

+545
-177
lines changed

evm/src/utils/SolverAccount.sol

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337U
1919
import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol";
2020
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
2121
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
22+
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
2223

2324
import {SelectOptions, IIntentGatewayV2} from "@hyperbridge/core/apps/IntentGatewayV2.sol";
2425

@@ -30,7 +31,7 @@ import {SelectOptions, IIntentGatewayV2} from "@hyperbridge/core/apps/IntentGate
3031
* contract account using EIP-7702.
3132
* @author Polytope Labs
3233
*/
33-
contract SolverAccount is Account, ERC7821 {
34+
contract SolverAccount is Account, ERC7821, IERC1271 {
3435
/**
3536
* @notice Standard length of an ECDSA signature (r: 32 bytes, s: 32 bytes, v: 1 byte)
3637
*/
@@ -128,6 +129,18 @@ contract SolverAccount is Account, ERC7821 {
128129
return ECDSA.recover(hash, signature) == address(this);
129130
}
130131

132+
/**
133+
* @notice ERC-1271 signature validation for EIP-7702 delegated accounts.
134+
* @dev Required so that protocols using OpenZeppelin's SignatureChecker (e.g. USDC's
135+
* EIP-2612 permit) can verify signatures from this account. Under EIP-7702 the
136+
* account has code, so SignatureChecker takes the ERC-1271 path instead of
137+
* ecrecover. Delegates to {_rawSignatureValidation} which performs ECDSA recovery
138+
* and checks that the recovered address equals address(this) (the delegating EOA).
139+
*/
140+
function isValidSignature(bytes32 hash, bytes calldata signature) external view override returns (bytes4) {
141+
return _rawSignatureValidation(hash, signature) ? bytes4(0x1626ba7e) : bytes4(0xffffffff);
142+
}
143+
131144
/**
132145
* @notice Validates an ERC-7821 authorized executor
133146
* @param caller The address of the caller

sdk/packages/sdk/src/configs/ChainConfigService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ export class ChainConfigService {
187187
return this.getConfig(chain)?.addresses.EntryPointV08!
188188
}
189189

190+
getCirclePaymasterV08Address(chain: string): HexString | undefined {
191+
return this.getConfig(chain)?.addresses.CirclePaymasterV08 as HexString | undefined
192+
}
193+
190194
getHyperbridgeAddress(): string {
191195
return hyperbridgeAddress
192196
}

sdk/packages/sdk/src/configs/chain.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ export interface ChainConfigData {
131131
UniswapV4PoolManager?: `0x${string}`
132132
/** Uniswap V4 StateView (canonical CREATE2 address) for pool state reads via extsload */
133133
UniswapV4StateView?: `0x${string}`
134+
/** Circle Paymaster v0.8 contract address (ERC-4337 onchain USDC paymaster) */
135+
CirclePaymasterV08?: `0x${string}`
134136
}
135137
rpcEnvKey?: string
136138
defaultRpcUrl?: string
@@ -250,6 +252,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
250252
UniswapV3Factory: "0x0000000000000000000000000000000000000000",
251253
Calldispatcher: "0xC7f13b6D03A0A7F3239d38897503E90553ABe155",
252254
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
255+
CirclePaymasterV08: "0x3BA9A96eE3eFf3A69E2B18886AcF52027EFF8966",
253256
},
254257
rpcEnvKey: "SEPOLIA",
255258
defaultRpcUrl: "https://1rpc.io/sepolia",
@@ -298,6 +301,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
298301
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
299302
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
300303
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
304+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
301305
Usdt0Oft: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
302306
},
303307
rpcEnvKey: "ETH_MAINNET",
@@ -410,6 +414,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
410414
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
411415
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
412416
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
417+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
413418
Usdt0Oft: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
414419
},
415420
rpcEnvKey: "ARBITRUM_MAINNET",
@@ -452,7 +457,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
452457
addresses: {
453458
IntentGateway: "0x1a4ee689a004b10210a1df9f24a387ea13359acf",
454459
IntentGatewayV2: "0x2d61624A17f361020679FaA16fbB566C344AaF4B",
455-
SolverAccount: "0xd4d594C99f23b1Fb9d65fdd9062854B1A1C5780b",
460+
SolverAccount: "0xb7d5Bb305Fd102C9B0a343978f3b9Accc00e9603",
456461
TokenGateway: "0xFd413e3AFe560182C4471F4d143A96d3e259B6dE",
457462
Host: "0x6FFe92e4d7a9D589549644544780e6725E84b248",
458463
UniswapRouter02: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24",
@@ -464,6 +469,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
464469
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
465470
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
466471
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
472+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
467473
AerodromeRouter: "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
468474
UniswapV4PositionManager: "0x7c5f5a4bbd8fd63184577525326123b519429bdc",
469475
UniswapV4PoolManager: "0x498581ff718922c3f8e6a244956af099b2652b2b",
@@ -525,6 +531,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
525531
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
526532
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
527533
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
534+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
528535
Usdt0Oft: "0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13",
529536
},
530537
rpcEnvKey: "POLYGON_MAINNET",
@@ -572,6 +579,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
572579
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
573580
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
574581
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
582+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
575583
Usdt0Oft: "0xc07be8994d035631c36fb4a89c918cefb2f03ec3",
576584
},
577585
rpcEnvKey: "UNICHAIN_MAINNET",
@@ -642,6 +650,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
642650
Calldispatcher: "0x876F1891982E260026630c233A4897160A281Fb8",
643651
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
644652
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
653+
CirclePaymasterV08: "0x3BA9A96eE3eFf3A69E2B18886AcF52027EFF8966",
645654
SolverAccount: "0xCDFcFeD7A14154846808FddC8Ba971A2f8a830a3",
646655
},
647656
rpcEnvKey: "POLYGON_AMOY",
@@ -675,6 +684,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
675684
Calldispatcher: "0xC71251c8b3e7B02697A84363Eef6DcE8DfBdF333",
676685
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
677686
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
687+
CirclePaymasterV08: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec",
678688
UniswapV4PositionManager: "0x3c3ea4b57a46241e54610e5f022e5c45859a1017",
679689
UniswapV4PoolManager: "0x9a13f98cb987694c9f086b1f5eb990eea8264ec3",
680690
},
@@ -756,6 +766,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
756766
TokenGateway: "0xFcDa26cA021d5535C3059547390E6cCd8De7acA6",
757767
Host: "0x3435bD7e5895356535459D6087D1eB982DAd90e7",
758768
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
769+
CirclePaymasterV08: "0x3BA9A96eE3eFf3A69E2B18886AcF52027EFF8966",
759770
},
760771
defaultRpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
761772
consensusStateId: "ETH0",
@@ -780,6 +791,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
780791
TokenGateway: "0xFcDa26cA021d5535C3059547390E6cCd8De7acA6",
781792
Host: "0x6d51b678836d8060d980605d2999eF211809f3C2",
782793
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
794+
CirclePaymasterV08: "0x3BA9A96eE3eFf3A69E2B18886AcF52027EFF8966",
783795
},
784796
defaultRpcUrl: "https://sepolia.optimism.io",
785797
consensusStateId: "ETH0",
@@ -804,6 +816,7 @@ export const chainConfigs: Record<number, ChainConfigData> = {
804816
TokenGateway: "0xFcDa26cA021d5535C3059547390E6cCd8De7acA6",
805817
Host: "0xD198c01839dd4843918617AfD1e4DDf44Cc3BB4a",
806818
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
819+
CirclePaymasterV08: "0x3BA9A96eE3eFf3A69E2B18886AcF52027EFF8966",
807820
},
808821
defaultRpcUrl: "https://sepolia.base.org",
809822
consensusStateId: "ETH0",

sdk/packages/sdk/src/protocols/intents/BidManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class BidManager {
6767
maxFeePerGas,
6868
maxPriorityFeePerGas,
6969
callData,
70+
paymasterAndData = "0x" as HexString,
7071
} = options
7172

7273
const chainId = BigInt(
@@ -84,7 +85,7 @@ export class BidManager {
8485
accountGasLimits,
8586
preVerificationGas,
8687
gasFees,
87-
paymasterAndData: "0x" as HexString,
88+
paymasterAndData,
8889
signature: "0x" as HexString,
8990
}
9091

sdk/packages/sdk/src/protocols/intents/CryptoUtils.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,16 @@ export class CryptoUtils {
248248
const factory = hasFactory ? (`0x${userOp.initCode.slice(2, 42)}` as HexString) : undefined
249249
const factoryData = hasFactory ? (`0x${userOp.initCode.slice(42)}` as HexString) : undefined
250250

251+
// EntryPoint v0.8 packed paymasterAndData layout:
252+
// paymaster (20 bytes) || paymasterVerificationGasLimit (uint128, 16 bytes)
253+
// || paymasterPostOpGasLimit (uint128, 16 bytes) || paymasterData (variable)
251254
const hasPaymaster =
252255
userOp.paymasterAndData && userOp.paymasterAndData !== "0x" && userOp.paymasterAndData.length > 2
253-
const paymaster = hasPaymaster ? (`0x${userOp.paymasterAndData.slice(2, 42)}` as HexString) : undefined
254-
const paymasterData = hasPaymaster ? (`0x${userOp.paymasterAndData.slice(42)}` as HexString) : undefined
256+
const pmHex = hasPaymaster ? userOp.paymasterAndData.slice(2) : ""
257+
const paymaster = hasPaymaster ? (`0x${pmHex.slice(0, 40)}` as HexString) : undefined
258+
const paymasterVerificationGasLimit = hasPaymaster ? BigInt(`0x${pmHex.slice(40, 72)}`) : undefined
259+
const paymasterPostOpGasLimit = hasPaymaster ? BigInt(`0x${pmHex.slice(72, 104)}`) : undefined
260+
const paymasterData = hasPaymaster ? (`0x${pmHex.slice(104)}` as HexString) : undefined
255261

256262
const userOpBundler: Record<string, unknown> = {
257263
sender: userOp.sender,
@@ -273,8 +279,8 @@ export class CryptoUtils {
273279
if (paymaster) {
274280
userOpBundler.paymaster = paymaster
275281
userOpBundler.paymasterData = paymasterData || "0x"
276-
userOpBundler.paymasterVerificationGasLimit = toHex(50_000n)
277-
userOpBundler.paymasterPostOpGasLimit = toHex(50_000n)
282+
userOpBundler.paymasterVerificationGasLimit = toHex(paymasterVerificationGasLimit!)
283+
userOpBundler.paymasterPostOpGasLimit = toHex(paymasterPostOpGasLimit!)
278284
}
279285

280286
return userOpBundler
@@ -317,9 +323,7 @@ export class CryptoUtils {
317323
* @throws If the bundler URL is not configured, the HTTP call fails, or any
318324
* individual response contains an error.
319325
*/
320-
async sendBundlerBatch<T extends unknown[]>(
321-
requests: { method: BundlerMethod; params: unknown[] }[],
322-
): Promise<T> {
326+
async sendBundlerBatch<T extends unknown[]>(requests: { method: BundlerMethod; params: unknown[] }[]): Promise<T> {
323327
if (!this.ctx.bundlerUrl) {
324328
throw new Error("Bundler URL not configured")
325329
}

sdk/packages/sdk/src/protocols/intents/GasEstimator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export class GasEstimator {
157157
let callGasLimit: bigint = 500_000n
158158
let verificationGasLimit: bigint = 100_000n
159159
let preVerificationGas: bigint = 100_000n
160+
// Circle Paymaster v0.8 caps used as fallback when the bundler doesn't return paymaster gas fields.
161+
let paymasterVerificationGasLimit: bigint = 0n
162+
let paymasterPostOpGasLimit: bigint = 0n
160163

161164
if (this.ctx.bundlerUrl) {
162165
try {
@@ -257,6 +260,14 @@ export class GasEstimator {
257260
verificationGasLimit = (BigInt(gasEstimate.verificationGasLimit) * 105n) / 100n
258261
preVerificationGas = (BigInt(gasEstimate.preVerificationGas) * 105n) / 100n
259262

263+
if (gasEstimate.paymasterVerificationGasLimit) {
264+
paymasterVerificationGasLimit =
265+
(BigInt(gasEstimate.paymasterVerificationGasLimit) * 105n) / 100n
266+
}
267+
if (gasEstimate.paymasterPostOpGasLimit) {
268+
paymasterPostOpGasLimit = (BigInt(gasEstimate.paymasterPostOpGasLimit) * 105n) / 100n
269+
}
270+
260271
if (pimlicoGasPrices) {
261272
const level = pimlicoGasPrices.fast ?? pimlicoGasPrices.standard ?? pimlicoGasPrices.slow ?? null
262273

@@ -275,8 +286,7 @@ export class GasEstimator {
275286
// Alchemy requires 25% priority fee buffer (0% for Arbitrum)
276287
const isArbitrum = chainId === 42161n
277288
const alchemyPrioBump = isArbitrum ? 0n : 25n
278-
maxPriorityFeePerGas =
279-
rundlerPriorityFee + (rundlerPriorityFee * alchemyPrioBump) / 100n
289+
maxPriorityFeePerGas = rundlerPriorityFee + (rundlerPriorityFee * alchemyPrioBump) / 100n
280290
// Alchemy recommends 50% base fee buffer
281291
const bufferedBaseFee = baseFeePerGas + (baseFeePerGas * 50n) / 100n
282292
maxFeePerGas = bufferedBaseFee + maxPriorityFeePerGas
@@ -301,7 +311,8 @@ export class GasEstimator {
301311
}
302312
}
303313

304-
const totalGas = callGasLimit + verificationGasLimit + preVerificationGas
314+
const totalGas =
315+
callGasLimit + verificationGasLimit + preVerificationGas + paymasterVerificationGasLimit + paymasterPostOpGasLimit
305316
const rawTotalGasCostWei = totalGas * maxFeePerGas
306317

307318
const totalGasInDestFeeToken = await convertGasToFeeToken(
@@ -323,6 +334,8 @@ export class GasEstimator {
323334
callGasLimit,
324335
verificationGasLimit,
325336
preVerificationGas,
337+
paymasterVerificationGasLimit,
338+
paymasterPostOpGasLimit,
326339
maxFeePerGas,
327340
maxPriorityFeePerGas,
328341
totalGasCostWei,

sdk/packages/sdk/src/types/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,11 @@ export interface SigningAccount {
11861186
signMessage: (messageHash: HexString, chainId: number) => Promise<HexString>
11871187
/** Signs a raw 32-byte hash, returning split signature components for EIP-7702 etc. */
11881188
signRawHash: (hash: HexString) => Promise<{ r: HexString; s: HexString; yParity: number }>
1189+
/**
1190+
* Signs an EIP-712 typed-data payload (e.g. an EIP-2612 USDC permit for the Circle paymaster).
1191+
* The shape of `typedData` matches viem's `TypedDataDefinition` (domain + types + message).
1192+
*/
1193+
signTypedData: (typedData: unknown, chainId?: number) => Promise<HexString>
11891194
}
11901195

11911196
export interface SubmitBidOptions {
@@ -1208,6 +1213,12 @@ export interface SubmitBidOptions {
12081213
maxPriorityFeePerGas: bigint
12091214
/** Pre-built ERC-7821 calldata encoding the UserOp execution (approvals + fillOrder). */
12101215
callData: HexString
1216+
/**
1217+
* Optional packed paymasterAndData for EntryPoint v0.8.
1218+
* Must be built BEFORE calling prepareSubmitBid so the hash covers paymaster bytes.
1219+
* Defaults to "0x" (EntryPoint deposit pays gas).
1220+
*/
1221+
paymasterAndData?: HexString
12111222
}
12121223

12131224
export interface EstimateFillOrderParams {
@@ -1237,6 +1248,10 @@ export interface FillOrderEstimate {
12371248
callGasLimit: bigint
12381249
verificationGasLimit: bigint
12391250
preVerificationGas: bigint
1251+
/** Paymaster verification gas limit from bundler estimate, or Circle's cap if absent. 0n when no paymaster. */
1252+
paymasterVerificationGasLimit: bigint
1253+
/** Paymaster postOp gas limit from bundler estimate, or Circle's cap if absent. 0n when no paymaster. */
1254+
paymasterPostOpGasLimit: bigint
12401255
maxFeePerGas: bigint
12411256
maxPriorityFeePerGas: bigint
12421257
totalGasCostWei: bigint

sdk/packages/simplex/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperbridge/simplex",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"license": "Apache-2.0",
55
"description": "IntentGateway simplex package for hyperbridge",
66
"main": "dist/index.js",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { erc20Abi } from "viem"
2+
3+
export const EIP2612_ABI = [
4+
...erc20Abi,
5+
{
6+
inputs: [{ internalType: "address", name: "owner", type: "address" }],
7+
stateMutability: "view",
8+
type: "function",
9+
name: "nonces",
10+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
11+
},
12+
{
13+
inputs: [],
14+
name: "version",
15+
outputs: [{ internalType: "string", name: "", type: "string" }],
16+
stateMutability: "view",
17+
type: "function",
18+
},
19+
] as const

0 commit comments

Comments
 (0)