|
| 1 | +<!doctype html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8"> |
| 5 | +<meta name="viewport" content="width=device-width,initial-scale=1"> |
| 6 | +<title>Seneschal Paymaster — pay gas in stablecoins on Base</title> |
| 7 | +<meta name="description" content="ERC-4337 v0.7 paymaster on Base mainnet. Pay transaction gas in USDC, EURC, USDT, DAI or cbBTC instead of ETH. Chainlink-priced, verified on BaseScan + Sourcify, smoke-tested via Pimlico."> |
| 8 | +<style> |
| 9 | +:root { |
| 10 | + --fg: #1a1a1a; |
| 11 | + --muted: #6b6b6b; |
| 12 | + --accent: #1462b3; |
| 13 | + --bg: #fdfdfc; |
| 14 | + --code-bg: #f4f4f4; |
| 15 | + --border: #e4e4e2; |
| 16 | + --good: #1f7a3a; |
| 17 | + --warn: #b06900; |
| 18 | +} |
| 19 | +* { box-sizing: border-box } |
| 20 | +html, body { background: var(--bg); color: var(--fg); margin: 0 } |
| 21 | +body { font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; padding: 1em } |
| 22 | +main { max-width: 52em; margin: 0 auto; padding: 2em 1em 4em } |
| 23 | +h1 { font-weight: 600; font-size: 2em; margin: 0 0 .1em } |
| 24 | +h2 { font-weight: 600; font-size: 1.3em; margin-top: 2em; padding-bottom: .15em; border-bottom: 1px solid var(--border) } |
| 25 | +h3 { font-weight: 600; font-size: 1.05em; margin-top: 1.5em } |
| 26 | +p { margin: .6em 0 } |
| 27 | +a { color: var(--accent) } |
| 28 | +.muted { color: var(--muted); font-size: 0.92em } |
| 29 | +code, pre { font-family: ui-monospace, "JetBrains Mono", Consolas, "Liberation Mono", monospace; font-size: 0.92em } |
| 30 | +code { background: var(--code-bg); padding: 1px 5px; border-radius: 3px; word-break: break-word } |
| 31 | +pre { background: var(--code-bg); padding: 0.8em 1em; border-radius: 4px; overflow-x: auto; line-height: 1.45 } |
| 32 | +pre code { background: transparent; padding: 0 } |
| 33 | +.kv { background: #fff; border: 1px solid var(--border); border-radius: 5px; padding: 0.6em 1em; margin: 0.6em 0 } |
| 34 | +.kv table { margin: 0 } |
| 35 | +table { border-collapse: collapse; width: 100%; margin: 0.8em 0; font-size: 0.92em } |
| 36 | +th, td { padding: 0.5em 0.7em; border: 1px solid var(--border); text-align: left; vertical-align: top } |
| 37 | +th { background: #f7f7f5 } |
| 38 | +.callout { background: #fff8e7; border-left: 3px solid #e0b400; padding: 0.6em 1em; margin: 1em 0; border-radius: 0 4px 4px 0 } |
| 39 | +.callout-info { background: #eaf3fc; border-left-color: #1462b3 } |
| 40 | +.callout-good { background: #ecf6ef; border-left-color: #1f7a3a } |
| 41 | +.pill { display: inline-block; font-size: 0.78em; font-weight: 600; padding: 1px 7px; border-radius: 3px; vertical-align: 2px } |
| 42 | +.pill.live { background: #ecf6ef; color: var(--good); border: 1px solid #c2e2ce } |
| 43 | +.pill.deprecated { background: #fdecec; color: #a23131; border: 1px solid #f0c1c1 } |
| 44 | +nav.crumbs { font-size: 0.9em; color: var(--muted); margin-bottom: 0.5em } |
| 45 | +nav.crumbs a { color: var(--muted) } |
| 46 | +footer { text-align: center; margin-top: 4em; padding-top: 2em; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.9em } |
| 47 | +</style> |
| 48 | +</head> |
| 49 | +<body> |
| 50 | +<main> |
| 51 | + |
| 52 | +<nav class="crumbs"> |
| 53 | +<a href="https://seneschal.space/">Seneschal</a> |
| 54 | + · <a href="https://docs.seneschal.space/">Docs</a> |
| 55 | + · Paymaster |
| 56 | +</nav> |
| 57 | + |
| 58 | +<h1>Seneschal Paymaster <span class="pill live">LIVE</span></h1> |
| 59 | +<p class="muted"> |
| 60 | +Pay ERC-4337 transaction gas in stablecoins on Base mainnet. No ETH |
| 61 | +required in your smart account; the paymaster fronts the gas and pulls |
| 62 | +USDC/EURC/USDT/DAI/cbBTC from you at Chainlink-quoted rates. |
| 63 | +</p> |
| 64 | + |
| 65 | +<div class="callout callout-info"> |
| 66 | +<strong>Status:</strong> v2 deployed, verified, profitable. |
| 67 | +Smoke-tested end-to-end via Pimlico. |
| 68 | +Source verified on <a href="https://basescan.org/address/0xb6E8d189285003cF0000388b01BA0C3433ee9f14#code">BaseScan</a> |
| 69 | +and <a href="https://sourcify.dev/#/lookup/0xb6E8d189285003cF0000388b01BA0C3433ee9f14">Sourcify</a> |
| 70 | +(<code>exact_match</code>). Integration questions: |
| 71 | +<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">GitHub issues</a> |
| 72 | +or <code>@Rotwang9000</code> on Discord. |
| 73 | +</div> |
| 74 | + |
| 75 | +<h2>Live address</h2> |
| 76 | +<div class="kv"> |
| 77 | +<table> |
| 78 | +<tr><th>Paymaster</th><td><code>0xb6E8d189285003cF0000388b01BA0C3433ee9f14</code></td></tr> |
| 79 | +<tr><th>Chain</th><td>Base mainnet (chainId 8453)</td></tr> |
| 80 | +<tr><th>EntryPoint</th><td><code>0x0000000071727De22E5E9d8BAf0edAc6f37da032</code> (ERC-4337 v0.7)</td></tr> |
| 81 | +<tr><th>Source</th><td><a href="https://github.com/Rotwang9000/seneschal-paymaster">Rotwang9000/seneschal-paymaster</a></td></tr> |
| 82 | +<tr><th>Verification</th><td><a href="https://basescan.org/address/0xb6E8d189285003cF0000388b01BA0C3433ee9f14#code">BaseScan</a> · <a href="https://sourcify.dev/#/lookup/0xb6E8d189285003cF0000388b01BA0C3433ee9f14">Sourcify</a></td></tr> |
| 83 | +<tr><th>Smoke-tested via</th><td>Pimlico (<code>api.pimlico.io/v2/base/rpc</code>)</td></tr> |
| 84 | +</table> |
| 85 | +</div> |
| 86 | + |
| 87 | +<p class="muted"> |
| 88 | +The earlier v1 paymaster at |
| 89 | +<code>0xC5642B37000F9559b4628D7BA228051010150017</code> |
| 90 | +<span class="pill deprecated">deprecated</span> is undeployed (deposit |
| 91 | +withdrawn). v1 under-charged for its own postOp gas; v2 fixes that with |
| 92 | +an explicit overhead inside <code>_postOp</code> and a <code>validUntil</code> |
| 93 | +window inside <code>_validatePaymasterUserOp</code>. |
| 94 | +</p> |
| 95 | + |
| 96 | +<h2>Supported tokens</h2> |
| 97 | +<table> |
| 98 | +<tr><th>Symbol</th><th>Address</th><th>Decimals</th><th>Markup over Chainlink spot</th></tr> |
| 99 | +<tr><td><code>USDC</code></td><td><code>0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913</code></td><td>6</td><td>7%</td></tr> |
| 100 | +<tr><td><code>EURC</code></td><td><code>0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42</code></td><td>6</td><td>7%</td></tr> |
| 101 | +<tr><td><code>USDT</code></td><td><code>0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2</code></td><td>6</td><td>8%</td></tr> |
| 102 | +<tr><td><code>DAI</code></td><td><code>0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb</code></td><td>18</td><td>8%</td></tr> |
| 103 | +<tr><td><code>cbBTC</code></td><td><code>0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf</code></td><td>8</td><td>10%</td></tr> |
| 104 | +</table> |
| 105 | +<p> |
| 106 | +Prices feed from Chainlink. Stablecoins use the canonical token/USD |
| 107 | +feeds with a 25h heartbeat tolerance; cbBTC uses a 2h tolerance. |
| 108 | +</p> |
| 109 | + |
| 110 | +<h2>paymasterAndData layout</h2> |
| 111 | +<pre><code>[ 0..20) paymaster address (set by EntryPoint) |
| 112 | +[20..36) paymasterVerificationGasLimit (recommended ≥ 200 000) |
| 113 | +[36..52) paymasterPostOpGasLimit (recommended ≥ 120 000) |
| 114 | +[52..72) token address (one of the supported tokens) |
| 115 | +[72..104) maxTokenCost (uint256) (max token-native units you'll spend)</code></pre> |
| 116 | + |
| 117 | +<p> |
| 118 | +<code>maxTokenCost</code> is a slippage guard. Quote off-chain with |
| 119 | +<code>paymaster.quote(token, expectedGasWei)</code> and supply 110-120% of |
| 120 | +that to absorb a baseFee uptick between simulation and inclusion. |
| 121 | +</p> |
| 122 | + |
| 123 | +<h3>Gas budget rationale</h3> |
| 124 | +<p> |
| 125 | +Validation reads <strong>two</strong> Chainlink feeds (ETH/USD + token/USD), |
| 126 | +executes a <code>transferFrom</code> from the sender, and emits the prefund |
| 127 | +context. Empirical gas: ~140–160k. PostOp reads the token feed again, |
| 128 | +executes a <code>transfer</code> of the refund, and emits an event. |
| 129 | +Empirical gas: ~55–75k. |
| 130 | +</p> |
| 131 | +<p> |
| 132 | +Setting <code>paymasterVerificationGasLimit</code> below 150 000 or |
| 133 | +<code>paymasterPostOpGasLimit</code> below 90 000 will cause |
| 134 | +Pimlico/Alchemy/Stackup to drop the userOp with <code>AA33 reverted 0x</code> |
| 135 | +(out-of-gas inside the paymaster). The contract refunds unused gas via |
| 136 | +the postOp refund mechanism, so generous budgets are essentially free. |
| 137 | +</p> |
| 138 | + |
| 139 | +<h3>validationData</h3> |
| 140 | +<p> |
| 141 | +<code>_validatePaymasterUserOp</code> returns a <code>validUntil</code> of |
| 142 | +<code>block.timestamp + 5 minutes</code>. Bundlers will therefore drop any |
| 143 | +userOp that doesn't get included within that window — important because |
| 144 | +the Chainlink-read prices in validation could otherwise drift. If you |
| 145 | +simulate a userOp and then sit on it for >5 minutes before submitting, |
| 146 | +re-simulate. |
| 147 | +</p> |
| 148 | + |
| 149 | +<h2>What the smart account must have</h2> |
| 150 | +<p>Before sending the userOp:</p> |
| 151 | +<ol> |
| 152 | +<li><strong>Token balance</strong>: at least <code>maxTokenCost</code> of the |
| 153 | +chosen token in the smart account.</li> |
| 154 | +<li><strong>Token allowance</strong>: the paymaster needs |
| 155 | +<code>approve(paymaster, maxTokenCost)</code> (or more — |
| 156 | +<code>MaxUint256</code> once is fine). The allowance can be granted either: |
| 157 | +<ul> |
| 158 | +<li>in a <strong>prior userOp</strong> the user signs (one-off "approve |
| 159 | +paymaster for unlimited USDC"), or</li> |
| 160 | +<li>in a <strong>batched call</strong> within the same userOp (if your |
| 161 | +account supports batched calls and your bundler allows |
| 162 | +<code>transferFrom</code>-from-sender during validation, which Pimlico |
| 163 | +does).</li> |
| 164 | +</ul> |
| 165 | +</li> |
| 166 | +</ol> |
| 167 | +<p> |
| 168 | +If the allowance or balance is insufficient, the bundler will drop the |
| 169 | +userOp during simulation. There's no on-chain failure path. |
| 170 | +</p> |
| 171 | + |
| 172 | +<h2>Live example — viem + permissionless</h2> |
| 173 | +<pre><code>import { createPublicClient, http, encodePacked, parseAbi } from 'viem'; |
| 174 | +import { base } from 'viem/chains'; |
| 175 | +import { createBundlerClient, entryPoint07Address } from 'viem/account-abstraction'; |
| 176 | + |
| 177 | +const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14'; |
| 178 | +const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; |
| 179 | + |
| 180 | +const pub = createPublicClient({ |
| 181 | + chain: base, |
| 182 | + transport: http('https://base-rpc.publicnode.com') |
| 183 | +}); |
| 184 | + |
| 185 | +const quote = await pub.readContract({ |
| 186 | + address: PAYMASTER, |
| 187 | + abi: parseAbi(['function quote(address,uint256) view returns (uint256)']), |
| 188 | + functionName: 'quote', |
| 189 | + args: [USDC, 900_000_000_000_000n] // 0.0009 ETH ≈ 300k gas @ 3 gwei |
| 190 | +}); |
| 191 | +const maxTokenCost = (quote * 12n) / 10n; // +20% slippage buffer |
| 192 | +const paymasterData = encodePacked(['address','uint256'], [USDC, maxTokenCost]); |
| 193 | + |
| 194 | +const bundler = createBundlerClient({ |
| 195 | + chain: base, |
| 196 | + transport: http('https://api.pimlico.io/v2/base/rpc?apikey=' + process.env.PIMLICO_KEY), |
| 197 | + paymaster: { |
| 198 | + async getPaymasterData() { |
| 199 | + return { |
| 200 | + paymaster: PAYMASTER, |
| 201 | + paymasterData, |
| 202 | + paymasterVerificationGasLimit: 200_000n, |
| 203 | + paymasterPostOpGasLimit: 120_000n |
| 204 | + }; |
| 205 | + } |
| 206 | + } |
| 207 | +}); |
| 208 | +// then bundler.sendUserOperation({ account, calls, … }) as usual.</code></pre> |
| 209 | + |
| 210 | +<h2>Live example — ethers.js v6</h2> |
| 211 | +<pre><code>import { ethers } from 'ethers'; |
| 212 | + |
| 213 | +const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14'; |
| 214 | +const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; |
| 215 | +const provider = new ethers.JsonRpcProvider('https://base-rpc.publicnode.com'); |
| 216 | + |
| 217 | +const paymaster = new ethers.Contract( |
| 218 | + PAYMASTER, |
| 219 | + [ 'function quote(address token, uint256 ethWei) view returns (uint256)' ], |
| 220 | + provider |
| 221 | +); |
| 222 | + |
| 223 | +// 1. Estimate the userOp's total gas (callGasLimit + verificationGasLimit |
| 224 | +// + preVerificationGas + paymasterVerificationGasLimit + paymasterPostOpGasLimit) |
| 225 | +// multiplied by maxFeePerGas. Most ERC-4337 SDKs expose this directly. |
| 226 | +const expectedGasWei = 300_000n * 3_000_000_000n; // 300k gas × 3 gwei |
| 227 | + |
| 228 | +// 2. Quote the USDC cost. |
| 229 | +const quote = await paymaster.quote(USDC, expectedGasWei); |
| 230 | +const maxTokenCost = (quote * 12n) / 10n; // 20% slippage buffer |
| 231 | + |
| 232 | +// 3. Build the paymasterAndData suffix (SDK / bundler prepends the |
| 233 | +// address + gas limits; you only contribute the trailing 52 bytes). |
| 234 | +const paymasterData = ethers.solidityPacked( |
| 235 | + [ 'address', 'uint256' ], |
| 236 | + [ USDC, maxTokenCost ] |
| 237 | +); |
| 238 | + |
| 239 | +// 4. In your bundler client (Pimlico, Stackup, Alchemy AA, biconomy SDK): |
| 240 | +// paymaster: PAYMASTER, |
| 241 | +// paymasterData: paymasterData, |
| 242 | +// paymasterVerificationGasLimit: 200_000, |
| 243 | +// paymasterPostOpGasLimit: 120_000,</code></pre> |
| 244 | + |
| 245 | +<h2>Validation rules (bundler will check)</h2> |
| 246 | +<p> |
| 247 | +The paymaster passes |
| 248 | +<a href="https://eips.ethereum.org/EIPS/eip-7562">ERC-7562</a> |
| 249 | +storage-access rules: |
| 250 | +</p> |
| 251 | +<ul> |
| 252 | +<li>Reads/writes only its own storage (<code>tokenConfigs</code>, |
| 253 | +<code>paused</code>, <code>supportedTokens</code>).</li> |
| 254 | +<li>Calls <code>transferFrom</code> on the user's chosen ERC-20, which is |
| 255 | +sender-associated storage |
| 256 | +(<code>balances[sender]</code>, |
| 257 | +<code>allowances[sender][paymaster]</code>) — explicitly whitelisted by |
| 258 | +4337.</li> |
| 259 | +<li>Calls Chainlink price feeds via <code>staticcall</code> only — read-only.</li> |
| 260 | +</ul> |
| 261 | +<p> |
| 262 | +If a bundler reports a simulation failure not listed below, open a |
| 263 | +<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">GitHub |
| 264 | +issue</a> with the bundler trace. |
| 265 | +</p> |
| 266 | + |
| 267 | +<h2>Common errors</h2> |
| 268 | +<table> |
| 269 | +<tr><th>Error</th><th>Cause</th><th>Fix</th></tr> |
| 270 | +<tr><td><code>AA33 reverted: TokenNotSupported</code></td><td>Token addr not configured</td><td>Use one from the table above</td></tr> |
| 271 | +<tr><td><code>AA33 reverted: MaxTokenCostExceeded</code></td><td>Your <code>maxTokenCost</code> was below the live quote</td><td>Re-quote and bump by ≥20%</td></tr> |
| 272 | +<tr><td><code>AA33 reverted: BadPaymasterDataLength</code></td><td><code>paymasterAndData</code> suffix isn't exactly 52 bytes</td><td>Re-pack with <code>encodePacked(['address','uint256'], …)</code></td></tr> |
| 273 | +<tr><td><code>AA33 reverted: StalePrice</code></td><td>Chainlink feed hasn't updated within the staleness window</td><td>Almost always a Chainlink outage — retry in a few minutes</td></tr> |
| 274 | +<tr><td><code>AA33 reverted: EnforcedPause</code></td><td>Paymaster is paused</td><td>Use a different paymaster until ops un-pauses</td></tr> |
| 275 | +<tr><td><code>AA34 ERC20InsufficientAllowance</code></td><td>User didn't approve the paymaster</td><td>Send an <code>approve(paymaster, MAX)</code> userOp first</td></tr> |
| 276 | +<tr><td><code>AA33 reverted 0x</code> (empty data)</td><td>Validation OOG (two Chainlink reads + <code>transferFrom</code> won't fit)</td><td>Set <code>paymasterVerificationGasLimit</code> to ≥ 200 000</td></tr> |
| 277 | +<tr><td><code>AA40 over verificationGasLimit</code></td><td>Validation gas limit too low</td><td>Set <code>paymasterVerificationGasLimit</code> to ≥ 200 000</td></tr> |
| 278 | +<tr><td><code>AA50 paymaster postOp ran OOG</code></td><td>postOp gas limit too low</td><td>Set <code>paymasterPostOpGasLimit</code> to ≥ 120 000</td></tr> |
| 279 | +</table> |
| 280 | + |
| 281 | +<h2>Quoting at scale</h2> |
| 282 | +<p> |
| 283 | +If you're embedding the paymaster in an SDK, batch-quote at startup and |
| 284 | +refresh every ~5 blocks. The on-chain <code>quote()</code> takes ~70k gas |
| 285 | +to read all 5 token configs + Chainlink feeds — fine for one-off, |
| 286 | +expensive for per-userOp. Cache the markup constants client-side and only |
| 287 | +refresh the Chainlink prices. |
| 288 | +</p> |
| 289 | +<p>We do not throttle reads of <code>quote()</code> — feel free to hammer it.</p> |
| 290 | + |
| 291 | +<h2>Trust model</h2> |
| 292 | +<p>You're trusting:</p> |
| 293 | +<ol> |
| 294 | +<li>The Chainlink ETH/USD + token/USD feeds on Base. Industry standard.</li> |
| 295 | +<li>Us (the paymaster owner) not to pause the contract maliciously while |
| 296 | +your userOp is in flight. The 1-block pause window between a bundler |
| 297 | +accepting and submitting your userOp is the only realistic risk; impact |
| 298 | +is your userOp drops, no funds lost.</li> |
| 299 | +<li>The contract's source matches the deployed bytecode. Both Sourcify |
| 300 | +(<code>exact_match</code>) and BaseScan verification confirm this — open |
| 301 | +the BaseScan link above and you can read the source there.</li> |
| 302 | +</ol> |
| 303 | + |
| 304 | +<p>You're <strong>not</strong> trusting us with:</p> |
| 305 | +<ul> |
| 306 | +<li>Your private keys (you sign the userOp).</li> |
| 307 | +<li>Your ETH (we only ever hold the EntryPoint float, never user ETH).</li> |
| 308 | +<li>Your tokens beyond <code>maxTokenCost</code> (validation reverts if our |
| 309 | +quote exceeds the bound you signed).</li> |
| 310 | +</ul> |
| 311 | + |
| 312 | +<h2>Why this exists</h2> |
| 313 | +<p> |
| 314 | +Seneschal runs an Ethereum mainnet block builder and a liquidation |
| 315 | +searcher. The paymaster is a side-project of the same operator, |
| 316 | +offered as a public good on Base: a small markup (7–10%) over the |
| 317 | +Chainlink spot price covers the operator's gas float and Chainlink |
| 318 | +read costs, with anything beyond that going toward keeping the builder |
| 319 | +running. |
| 320 | +</p> |
| 321 | +<p> |
| 322 | +Open-source, MIT-licensed, no SDK lock-in. The contract is roughly |
| 323 | +350 lines of Solidity; you can read every line before integrating. |
| 324 | +</p> |
| 325 | + |
| 326 | +<h2>Support & status</h2> |
| 327 | +<ul> |
| 328 | +<li><strong>Issues / questions</strong>: |
| 329 | +<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">github.com/Rotwang9000/seneschal-paymaster/issues</a></li> |
| 330 | +<li><strong>Builder + liquidation stats</strong>: |
| 331 | +<a href="https://stats.seneschal.space">stats.seneschal.space</a> |
| 332 | +(paymaster KPIs land here once v2 has a few hundred userOps of history)</li> |
| 333 | +<li><strong>Operator address</strong>: |
| 334 | +<code>0x46Ba634261566CF242c853d1f49511f9268ba674</code>. |
| 335 | +ETH sent here funds the EntryPoint working float — no obligation, but |
| 336 | +appreciated if our paymaster saved you bundler grief.</li> |
| 337 | +</ul> |
| 338 | + |
| 339 | +<footer> |
| 340 | +<p>Seneschal · <a href="https://seneschal.space/">seneschal.space</a> · <a href="https://docs.seneschal.space/">data API docs</a> · <a href="https://stats.seneschal.space/">live stats</a></p> |
| 341 | +</footer> |
| 342 | + |
| 343 | +</main> |
| 344 | +</body> |
| 345 | +</html> |
0 commit comments