@gblin-protocol/mcp-server 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -10,20 +10,20 @@
10
10
  * 5. analyze_treasury_health → balances + gas check + runway estimate (PAID)
11
11
  * 6. get_governance_state → verify 48h timelock ownership + pending ops
12
12
  * 7. share_skill_with_peer → portable JSON skill seed + referral code
13
- * 8. find_keeper_bounty → check rebalance bounty availability + calldata (PAID)
13
+ * 8. get_auction_state → the rebalancing auction: side, gap and premium per row, with the bid
14
14
  * 9. get_market_risk_regime → on-chain BTC/ETH risk regime signal (PAID)
15
15
  * 10. verify_risk_attestation → verify a perishable Risk Attestation (FREE)
16
16
  */
17
- import { encodeFunctionData, formatUnits, getAddress, hashTypedData, isAddress, parseUnits, recoverTypedDataAddress, } from "viem";
17
+ import { encodeFunctionData, formatUnits, getAddress, hashTypedData, isAddress, parseUnits, recoverTypedDataAddress, encodeAbiParameters, keccak256, } from "viem";
18
18
  import { parseAbi } from "viem";
19
19
  import { z } from "zod";
20
- import { ERC20_ABI, GBLIN_ABI, TIMELOCK_ABI } from "./abi.js";
20
+ import { ERC20_ABI, GBLIN_ABI, TIMELOCK_ABI, LENS_ABI, ZAP_ABI, } from "./abi.js";
21
21
  import { client, getOnChainTimestamp } from "./client.js";
22
22
  import { requirePayment } from "./paywall.js";
23
- import { EXPECTED_MIN_DELAY_SECONDS, GBLIN_ATTESTOR, GBLIN_GUARDIAN, GBLIN_TIMELOCK, GBLIN_V6, MIN_DEPOSIT_WEI, USDC, WETH, WETH_USDC_POOL_FEE, } from "./config.js";
23
+ import { EXPECTED_MIN_DELAY_SECONDS, GBLIN_ATTESTOR, GBLIN_GUARDIAN, GBLIN_TIMELOCK, GBLIN_LENS, GBLIN_PREVIOUS, GBLIN_VAULT, GBLIN_ZAP, USDC, WETH, WETH_USDC_POOL_FEE, } from "./config.js";
24
24
  import { applySlippageBuffer, checkCooldown, getBasketState, getDynamicSlippage, getEthPriceUsd, getNavUsd, getWalletBalances, quoteGblinForUsdc, } from "./helpers.js";
25
25
  import { PACKAGE_VERSION } from "./config.js";
26
- import { findKeeperBounty } from "./keeper.js";
26
+ import { getAuctionState } from "./auction.js";
27
27
  import { RECEIPT_TOOL_DEFINITIONS, RECEIPT_TOOL_HANDLERS } from "./receipts.js";
28
28
  // ───────────────────────────────────────────────────────────────────────────
29
29
  // SwapRouter02 (Uniswap V3 on Base) — used for USDC→WETH exactInputSingle
@@ -36,6 +36,13 @@ const SWAP_ROUTER_ABI = parseAbi([
36
36
  // ERC-8021 Builder Code attribution (Base Builder Rewards)
37
37
  // ───────────────────────────────────────────────────────────────────────────
38
38
  const BUILDER_CODE_SUFFIX = "62635f6762646f33326a300b0080218021802180218021802180218021";
39
+ // Routing data the Zap hands to its swap adapter: the Uniswap V3 fee tier of the pair, ABI-encoded.
40
+ const VENUE_FEE_DATA = encodeAbiParameters([{ type: "uint24" }], [WETH_USDC_POOL_FEE]);
41
+ /** One routing entry per basket row, index for index; WETH and abandoned rows ignore theirs. */
42
+ async function venueDataPerRow() {
43
+ const n = await client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "basketLength", args: [GBLIN_VAULT] }).catch(() => 3n);
44
+ return Array.from({ length: Number(n) }, () => VENUE_FEE_DATA);
45
+ }
39
46
  function appendBuilderCode(calldata) {
40
47
  // Strip 0x if present, append suffix, restore 0x prefix
41
48
  const hex = calldata.startsWith("0x") ? calldata.slice(2) : calldata;
@@ -90,15 +97,21 @@ export const GET_TREASURY_STATE_DEFINITION = {
90
97
  };
91
98
  export async function handleGetTreasuryState() {
92
99
  try {
93
- const [navUsd, ethPriceUsd, basket, slippage] = await Promise.all([
100
+ const [navUsd, ethPriceUsd, basket, slippage, navReliable, mgmtFeeBps, openedAt] = await Promise.all([
94
101
  getNavUsd(),
95
102
  getEthPriceUsd(),
96
103
  getBasketState(),
97
104
  getDynamicSlippage(),
105
+ client.readContract({ address: GBLIN_VAULT, abi: GBLIN_ABI, functionName: "isNavReliable" }).catch(() => false),
106
+ client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "managementFeeBps", args: [GBLIN_VAULT] }).catch(() => 0n),
107
+ client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "auctionOpenedAt", args: [GBLIN_VAULT] }).catch(() => 0n),
98
108
  ]);
99
109
  return toolResult({
100
110
  nav_usd: Number(navUsd.toFixed(6)),
101
111
  eth_price_usd: Number(ethPriceUsd.toFixed(2)),
112
+ nav_reliable: navReliable,
113
+ management_fee_bps_per_year: Number(mgmtFeeBps),
114
+ auction_open: openedAt !== 0n,
102
115
  crash_shield_active: basket.crashShieldActive,
103
116
  slippage_buffer_pct: slippage.pct,
104
117
  slippage_reason: slippage.reason,
@@ -108,10 +121,10 @@ export async function handleGetTreasuryState() {
108
121
  base_weight_pct: e.baseWeightBps / 100,
109
122
  dynamic_weight_pct: e.dynamicWeightBps / 100,
110
123
  slashed: e.isSlashed,
111
- pool_fee_bps: e.poolFee,
112
124
  })),
113
125
  meta: {
114
- contract: GBLIN_V6,
126
+ vault: GBLIN_VAULT,
127
+ lens: GBLIN_LENS,
115
128
  chain: "base",
116
129
  chain_id: 8453,
117
130
  },
@@ -156,14 +169,11 @@ export async function handleQuoteSafeSwap(args) {
156
169
  const slippage = await getDynamicSlippage();
157
170
  const amountWei = parseUnits(parsed.amount_in, 18);
158
171
  if (parsed.direction === "buy") {
159
- if (amountWei < MIN_DEPOSIT_WEI) {
160
- return toolError(`DepositTooSmall: minimum buy is ${formatUnits(MIN_DEPOSIT_WEI, 18)} ETH.`, "Increase amount_in or pool buys into batches.");
161
- }
162
- const [gblinOut, founderFee, stabFee] = await client.readContract({
163
- address: GBLIN_V6,
164
- abi: GBLIN_ABI,
165
- functionName: "quoteBuyGBLIN",
166
- args: [amountWei],
172
+ const [gblinOut, protocolFee, stabFee] = await client.readContract({
173
+ address: GBLIN_LENS,
174
+ abi: LENS_ABI,
175
+ functionName: "quoteBuy",
176
+ args: [GBLIN_VAULT, amountWei],
167
177
  });
168
178
  const safeMin = applySlippageBuffer(gblinOut, slippage.bps);
169
179
  return toolResult({
@@ -172,7 +182,7 @@ export async function handleQuoteSafeSwap(args) {
172
182
  expected_gblin_out: formatUnits(gblinOut, 18),
173
183
  safe_min_gblin_out: formatUnits(safeMin, 18),
174
184
  fees: {
175
- founder_eth: formatUnits(founderFee, 18),
185
+ protocol_eth: formatUnits(protocolFee, 18),
176
186
  stability_eth: formatUnits(stabFee, 18),
177
187
  total_fee_bps: 10,
178
188
  },
@@ -184,10 +194,10 @@ export async function handleQuoteSafeSwap(args) {
184
194
  }
185
195
  // sell
186
196
  const ethOut = await client.readContract({
187
- address: GBLIN_V6,
188
- abi: GBLIN_ABI,
189
- functionName: "quoteSellGBLIN",
190
- args: [amountWei],
197
+ address: GBLIN_LENS,
198
+ abi: LENS_ABI,
199
+ functionName: "quoteSell",
200
+ args: [GBLIN_VAULT, amountWei],
191
201
  });
192
202
  const safeMin = applySlippageBuffer(ethOut, slippage.bps);
193
203
  return toolResult({
@@ -197,8 +207,8 @@ export async function handleQuoteSafeSwap(args) {
197
207
  safe_min_eth_out: formatUnits(safeMin, 18),
198
208
  slippage_buffer_bps: Number(slippage.bps),
199
209
  slippage_reason: slippage.reason,
200
- cooldown_note: "Sell reverts with CooldownActive if last buy was <2 min ago. Check via analyze_treasury_health.",
201
- next_step: "Call contract.sellGBLINForEth(amount_in, safe_min_eth_out).",
210
+ cooldown_note: "A redemption reverts with CooldownActive inside the vault's cooldown after your own mint (read live by analyze_treasury_health).",
211
+ next_step: "Redeem in kind with vault.sellGBLIN(amount_in) (no fee, no price feed), or exit to ETH through the Zap: approve the shares to it, then GBLINZap.sellGBLINForEth(amount_in, safe_min_eth_out, venueData, receiver) — all or nothing.",
202
212
  });
203
213
  }
204
214
  catch (err) {
@@ -214,7 +224,7 @@ const JitSwapSchema = z.object({
214
224
  });
215
225
  export const JIT_SWAP_DEFINITION = {
216
226
  name: "swap_gblin_to_usdc_jit",
217
- description: "When an x402 invoice (or any USDC obligation) arrives and your treasury sits in GBLIN, call this to get ready-to-broadcast calldata that redeems exactly the USDC you need, just in time. Two sequential transactions: (1) redeem GBLIN to ETH directly against the contract (sellGBLINForEth), (2) a Uniswap WETH->USDC swap. EOAs sign twice; ERC-4337 / EIP-7702 wallets can batch both into one UserOp. MEV-safe minOut included, never zero. Free to call — revenue is captured on-chain via the 0.05% founder fee on every swap.",
227
+ description: "When an x402 invoice (or any USDC obligation) arrives and your treasury sits in GBLIN, call this to get ready-to-broadcast calldata that redeems exactly the USDC you need, just in time. Two sequential transactions: (1) redeem GBLIN to ETH directly against the contract (sellGBLINForEth), (2) a Uniswap WETH->USDC swap. EOAs sign twice; ERC-4337 / EIP-7702 wallets can batch both into one UserOp. MEV-safe minOut included, never zero. Free to call. A redemption carries no protocol fee; the costs are the Zap's swaps and gas.",
218
228
  inputSchema: {
219
229
  type: "object",
220
230
  properties: {
@@ -244,26 +254,34 @@ export async function handleJitSwap(args) {
244
254
  }
245
255
  // 2. Reverse quote: how much GBLIN to sell?
246
256
  const quote = await quoteGblinForUsdc(parsed.usdc_needed);
247
- // 3a. Quote the ETH leg: how much ETH sellGBLINForEth returns for that GBLIN.
257
+ // 3a. Quote the ETH leg through the Lens: what the vault would pay for that many shares.
248
258
  const ethExpected = await client.readContract({
249
- address: GBLIN_V6,
250
- abi: GBLIN_ABI,
251
- functionName: "quoteSellGBLIN",
252
- args: [quote.gblinToSell],
259
+ address: GBLIN_LENS,
260
+ abi: LENS_ABI,
261
+ functionName: "quoteSell",
262
+ args: [GBLIN_VAULT, quote.gblinToSell],
253
263
  });
254
264
  const minEthOut = applySlippageBuffer(ethExpected, quote.slippage.bps);
255
265
  if (minEthOut === 0n) {
256
266
  return toolError("Quote returned zero ETH out - oracle stale or amount too small.");
257
267
  }
258
- // V6 removed sellGBLINForToken. GBLIN -> USDC is now two steps:
259
- // TX1: sellGBLINForEth(gblin, minEthOut) -> agent receives ETH
260
- // TX2: Uniswap exactInputSingle WETH->USDC, paid with the ETH received.
261
- // TX2 amountIn = minEthOut (the GUARANTEED minimum from TX1), so the second tx
262
- // can never request more ETH than the first actually delivered.
263
- const sellCalldata = encodeFunctionData({
268
+ // The vault redeems in kind and never swaps, so GBLIN -> USDC is three steps:
269
+ // TX1: approve the shares to the Zap (it pulls them);
270
+ // TX2: Zap.sellGBLINForEth(shares, minEthOut, venueData, receiver): redeem in kind on the vault,
271
+ // sell every leg, deliver ETH — all or nothing, so a leg that cannot be sold reverts the
272
+ // whole step instead of paying out less;
273
+ // TX3: Uniswap exactInputSingle WETH->USDC, paid with the ETH received.
274
+ // TX3 amountIn = minEthOut (the GUARANTEED minimum from TX2), so the last tx can never request
275
+ // more ETH than the previous one actually delivered.
276
+ const approveSharesCalldata = encodeFunctionData({
264
277
  abi: GBLIN_ABI,
278
+ functionName: "approve",
279
+ args: [GBLIN_ZAP, quote.gblinToSell],
280
+ });
281
+ const sellCalldata = encodeFunctionData({
282
+ abi: ZAP_ABI,
265
283
  functionName: "sellGBLINForEth",
266
- args: [quote.gblinToSell, minEthOut],
284
+ args: [quote.gblinToSell, minEthOut, await venueDataPerRow(), parsed.wallet_address],
267
285
  });
268
286
  const swapCalldata = encodeFunctionData({
269
287
  abi: SWAP_ROUTER_ABI,
@@ -285,13 +303,20 @@ export async function handleJitSwap(args) {
285
303
  steps: [
286
304
  {
287
305
  step: 1,
288
- description: "Redeem GBLIN to ETH on the GBLIN contract (sellGBLINForEth)",
289
- target: GBLIN_V6,
290
- calldata: appendBuilderCode(sellCalldata),
306
+ description: "Approve the shares to the GBLIN Zap",
307
+ target: GBLIN_VAULT,
308
+ calldata: appendBuilderCode(approveSharesCalldata),
291
309
  value: "0",
292
310
  },
293
311
  {
294
312
  step: 2,
313
+ description: "Redeem in kind and sell every leg for ETH through the Zap (all or nothing)",
314
+ target: GBLIN_ZAP,
315
+ calldata: appendBuilderCode(sellCalldata),
316
+ value: "0",
317
+ },
318
+ {
319
+ step: 3,
295
320
  description: "Swap the received ETH to USDC via Uniswap V3 (WETH->USDC)",
296
321
  target: SWAP_ROUTER_02,
297
322
  calldata: swapCalldata,
@@ -315,7 +340,7 @@ export async function handleJitSwap(args) {
315
340
  eoa: true,
316
341
  erc4337: true,
317
342
  eip7702: true,
318
- note: "V6 path is two steps (sellGBLINForEth + Uniswap WETH->USDC). EOAs sign twice; ERC-4337 / EIP-7702 wallets can batch both into one UserOp for atomicity. Both legs carry a minOut, so there is no sandwich surface.",
343
+ note: "Three steps: approve, the Zap exit, the WETH->USDC swap. EOAs sign three times; ERC-4337 / EIP-7702 wallets can batch all of them into one UserOp.",
319
344
  },
320
345
  gas_hint: 600_000,
321
346
  });
@@ -333,7 +358,7 @@ const InvestSchema = z.object({
333
358
  });
334
359
  export const INVEST_DEFINITION = {
335
360
  name: "invest_usdc_to_gblin",
336
- description: "When your agent's idle USDC exceeds operating needs (rule of thumb: more than 7x daily burn), call this to park the SURPLUS into GBLIN — managed crypto exposure minted at NAV directly from the contract, redeemable any time via swap_gblin_to_usdc_jit. Returns two steps of calldata: (1) approve USDC to the GBLIN contract, (2) buyGBLINInKind(USDC, amount, minGblinOut). No Uniswap leg, MEV-safe minOut, never zero. Free to call - revenue is captured on-chain via the 0.05% founder fee on every buy.",
361
+ description: "When your agent's idle USDC exceeds operating needs (rule of thumb: more than 7x daily burn), call this to park the SURPLUS into GBLIN — managed crypto exposure minted at NAV directly from the contract, redeemable any time via swap_gblin_to_usdc_jit. Returns two steps of calldata: (1) approve USDC to the GBLIN contract, (2) buyGBLINInKind(USDC, amount, minGblinOut). No Uniswap leg, MEV-safe minOut, never zero. Free to call. An in-kind deposit pays a fee of 0.50% to 2.00% (the floor, plus a tax when the deposit moves the USDC row away from its target); 0.05% of it is minted as shares to the fee recipient and the rest stays in the vault.",
337
362
  inputSchema: {
338
363
  type: "object",
339
364
  properties: {
@@ -373,46 +398,45 @@ export async function handleInvest(args) {
373
398
  const ethPriceScaled = BigInt(Math.round(ethPriceUsd * 1_000_000)); // 6 decimals
374
399
  const wethExpected = (usdcUnits * parseUnits("1", 18)) / ethPriceScaled;
375
400
  const minWethOut = applySlippageBuffer(wethExpected, slippage.bps);
376
- if (minWethOut < MIN_DEPOSIT_WEI) {
377
- return toolError(`DepositTooSmall: ${parsed.usdc_amount} USDC converts to ~${formatUnits(wethExpected, 18)} WETH, below contract minimum of ${formatUnits(MIN_DEPOSIT_WEI, 18)} ETH.`, "Aggregate more USDC before investing.");
378
- }
379
- // Quote GBLIN out: value the USDC in ETH terms (USDC ~ $1) via the contract's
380
- // own buy quote, then buffer with dynamic slippage so the call won't revert.
401
+ // Quote the shares for that much WETH through the Lens, then buffer with the dynamic slippage so
402
+ // the mint does not revert on a small move between quote and block.
381
403
  const [gblinExpected] = await client.readContract({
382
- address: GBLIN_V6,
383
- abi: GBLIN_ABI,
384
- functionName: "quoteBuyGBLIN",
385
- args: [wethExpected],
404
+ address: GBLIN_LENS,
405
+ abi: LENS_ABI,
406
+ functionName: "quoteBuy",
407
+ args: [GBLIN_VAULT, wethExpected],
386
408
  });
387
409
  const minGblinOut = applySlippageBuffer(gblinExpected, slippage.bps);
388
- // V6 buys USDC DIRECTLY (in-kind) - no Uniswap swap needed. Two steps only:
389
- // 1) approve USDC to the GBLIN contract
390
- // 2) buyGBLINInKind(USDC, amountIn, minGblinOut)
410
+ // Two steps, through the Zap: approve USDC to it, then one call that swaps USDC -> WETH on the
411
+ // adapter and mints at NAV. The allowance goes to the Zap, never to the vault, and both bounds
412
+ // travel with the call: minWethOut on the swap, minGblinOut on the mint. (Depositing USDC in kind
413
+ // directly on the vault also works, but pays the in-kind floor of 0.50% plus a deviation tax; the
414
+ // Zap path pays the 0.10% mint fee plus the pool's own cost.)
391
415
  const approveUsdcCalldata = encodeFunctionData({
392
416
  abi: ERC20_ABI,
393
417
  functionName: "approve",
394
- args: [GBLIN_V6, usdcUnits],
418
+ args: [GBLIN_ZAP, usdcUnits],
395
419
  });
396
420
  const buyCalldata = encodeFunctionData({
397
- abi: GBLIN_ABI,
398
- functionName: "buyGBLINInKind",
399
- args: [USDC, usdcUnits, minGblinOut],
421
+ abi: ZAP_ABI,
422
+ functionName: "buyGBLINWithToken",
423
+ args: [USDC, usdcUnits, minWethOut, minGblinOut, VENUE_FEE_DATA, parsed.wallet_address],
400
424
  });
401
425
  return toolResult({
402
426
  action: "sequential_txs",
403
427
  steps: [
404
428
  {
405
429
  step: 1,
406
- description: "Approve USDC to the GBLIN V6 contract",
430
+ description: "Approve USDC to the GBLIN Zap",
407
431
  target: USDC,
408
432
  calldata: appendBuilderCode(approveUsdcCalldata),
409
433
  value: "0",
410
434
  },
411
435
  {
412
436
  step: 2,
413
- description: "Buy GBLIN directly with USDC (V6 in-kind mint, no swap)",
414
- target: GBLIN_V6,
415
- calldata: buyCalldata,
437
+ description: "Swap USDC to WETH and mint GBLIN at NAV, in one transaction",
438
+ target: GBLIN_ZAP,
439
+ calldata: appendBuilderCode(buyCalldata),
416
440
  value: "0",
417
441
  },
418
442
  ],
@@ -424,7 +448,7 @@ export async function handleInvest(args) {
424
448
  security: {
425
449
  mev_protected: true,
426
450
  min_outs_set: true,
427
- note: "minGblinOut > 0, computed from the on-chain quote + dynamic slippage. Reverts on bad execution. No Uniswap swap - USDC is deposited directly as in-kind collateral.",
451
+ note: "minWethOut and minGblinOut are both above zero, computed from the oracle price and the Lens quote plus the dynamic slippage. Either bound failing reverts the whole call.",
428
452
  },
429
453
  });
430
454
  }
@@ -564,7 +588,7 @@ const GovernanceStateSchema = z.object({
564
588
  });
565
589
  export const GET_GOVERNANCE_STATE_DEFINITION = {
566
590
  name: "get_governance_state",
567
- description: "Verify GBLIN protocol governance state: confirms whether GBLIN_V6 is owned by the 48h Timelock, reads the timelock's min delay and grace period, reports role member counts, and surfaces any pending asset-addition proposal on the index contract. If an operation_id is provided, also reports the status of that specific timelock operation. Read-only — use this to gate trust-sensitive agent actions.",
591
+ description: "Verify GBLIN protocol governance state: confirms whether the GBLIN vault is owned by the 48h Timelock, reads the timelock's min delay and grace period, reports role member counts, and surfaces any pending asset-addition proposal on the index contract. If an operation_id is provided, also reports the status of that specific timelock operation. Read-only — use this to gate trust-sensitive agent actions.",
568
592
  inputSchema: {
569
593
  type: "object",
570
594
  properties: {
@@ -585,22 +609,23 @@ export async function handleGetGovernanceState(args) {
585
609
  return toolError(`Invalid arguments: ${e.message}`);
586
610
  }
587
611
  try {
588
- // 1. Read owner + pending asset from GBLIN_V6
589
- const [owner, founder, proposedAsset] = await Promise.all([
612
+ // 1. Owner, pending owner and fee recipient of the vault.
613
+ const [owner, pendingOwner, feeRecipient] = await Promise.all([
590
614
  client.readContract({
591
- address: GBLIN_V6,
615
+ address: GBLIN_VAULT,
592
616
  abi: GBLIN_ABI,
593
617
  functionName: "owner",
594
618
  }),
595
619
  client.readContract({
596
- address: GBLIN_V6,
620
+ address: GBLIN_VAULT,
597
621
  abi: GBLIN_ABI,
598
- functionName: "founderWallet",
622
+ functionName: "pendingOwner",
599
623
  }),
600
624
  client.readContract({
601
- address: GBLIN_V6,
602
- abi: GBLIN_ABI,
603
- functionName: "proposedAsset",
625
+ address: GBLIN_LENS,
626
+ abi: LENS_ABI,
627
+ functionName: "feeRecipient",
628
+ args: [GBLIN_VAULT],
604
629
  }),
605
630
  ]);
606
631
  const ownerNorm = getAddress(owner);
@@ -641,17 +666,13 @@ export async function handleGetGovernanceState(args) {
641
666
  // OZ TimelockController v5 uses AccessControl (not AccessControlEnumerable),
642
667
  // so getRoleMemberCount is unavailable. Use hasRole on known addresses instead.
643
668
  const ZERO = "0x0000000000000000000000000000000000000000";
644
- const founderAddr = await client.readContract({
645
- address: GBLIN_V6,
646
- abi: GBLIN_ABI,
647
- functionName: "founderWallet",
648
- });
649
- const [founderIsProposer, guardianIsCanceller, timelockIsSelfAdmin, executorOpen,] = await Promise.all([
669
+ const feeRecipientAddr = feeRecipient;
670
+ const [feeRecipientIsProposer, guardianIsCanceller, timelockIsSelfAdmin, executorOpen,] = await Promise.all([
650
671
  client.readContract({
651
672
  address: GBLIN_TIMELOCK,
652
673
  abi: TIMELOCK_ABI,
653
674
  functionName: "hasRole",
654
- args: [proposerRole, founderAddr],
675
+ args: [proposerRole, feeRecipientAddr],
655
676
  }),
656
677
  // Guardian multisig should hold CANCELLER_ROLE (veto power)
657
678
  client.readContract({
@@ -682,7 +703,7 @@ export async function handleGetGovernanceState(args) {
682
703
  min_delay_matches_expected: minDelay === EXPECTED_MIN_DELAY_SECONDS,
683
704
  expected_min_delay_seconds: Number(EXPECTED_MIN_DELAY_SECONDS),
684
705
  roles: {
685
- founder_is_proposer: founderIsProposer,
706
+ fee_recipient_is_proposer: feeRecipientIsProposer,
686
707
  guardian_is_canceller: guardianIsCanceller,
687
708
  timelock_is_self_admin: timelockIsSelfAdmin,
688
709
  executor_open_to_anyone: executorOpen,
@@ -758,40 +779,63 @@ export async function handleGetGovernanceState(args) {
758
779
  };
759
780
  }
760
781
  }
761
- // 4. Pending asset proposal inside GBLIN_V6 itself (separate 48h mini-timelock)
762
- const [pToken, pOracle, pPoolFee, pIsStable, pBaseWeight, pExecuteAfter] = proposedAsset;
763
- const hasPendingAsset = pExecuteAfter > 0n;
764
- const nowSec = Math.floor(Date.now() / 1000);
765
- const pendingAssetProposal = hasPendingAsset
766
- ? {
767
- token: getAddress(pToken),
768
- oracle: getAddress(pOracle),
769
- pool_fee_bps: pPoolFee,
770
- is_stable: pIsStable,
771
- base_weight_bps: Number(pBaseWeight),
772
- execute_after_unix: Number(pExecuteAfter),
773
- execute_after_iso: new Date(Number(pExecuteAfter) * 1000).toISOString(),
774
- seconds_until_executable: Number(pExecuteAfter) > nowSec ? Number(pExecuteAfter) - nowSec : 0,
782
+ // 4. A pending handover. Ownership moves in two steps (transferOwnership, then acceptOwnership by
783
+ // the new owner), and the timelock can only accept through a scheduled operation. When the
784
+ // pending owner is the timelock, the operation id is deterministic — TimelockController v5:
785
+ // keccak256(abi.encode(target, value, data, predecessor, salt)) — so its state can be reported.
786
+ const pendingOwnerNorm = getAddress(pendingOwner);
787
+ const handoverPending = pendingOwnerNorm !== "0x0000000000000000000000000000000000000000";
788
+ let pendingHandover = null;
789
+ if (handoverPending) {
790
+ pendingHandover = { pending_owner: pendingOwnerNorm, pending_owner_is_timelock: pendingOwnerNorm === timelockNorm };
791
+ if (pendingOwnerNorm === timelockNorm) {
792
+ const ZERO32 = `0x${"0".repeat(64)}`;
793
+ const acceptCalldata = encodeFunctionData({ abi: GBLIN_ABI, functionName: "acceptOwnership" });
794
+ const opId = keccak256(encodeAbiParameters([{ type: "address" }, { type: "uint256" }, { type: "bytes" }, { type: "bytes32" }, { type: "bytes32" }], [GBLIN_VAULT, 0n, acceptCalldata, ZERO32, ZERO32]));
795
+ try {
796
+ const [isPending, isReady, isDone, ts] = await Promise.all([
797
+ client.readContract({ address: GBLIN_TIMELOCK, abi: TIMELOCK_ABI, functionName: "isOperationPending", args: [opId] }),
798
+ client.readContract({ address: GBLIN_TIMELOCK, abi: TIMELOCK_ABI, functionName: "isOperationReady", args: [opId] }),
799
+ client.readContract({ address: GBLIN_TIMELOCK, abi: TIMELOCK_ABI, functionName: "isOperationDone", args: [opId] }),
800
+ client.readContract({ address: GBLIN_TIMELOCK, abi: TIMELOCK_ABI, functionName: "getTimestamp", args: [opId] }),
801
+ ]);
802
+ const tsNum = Number(ts);
803
+ pendingHandover.accept_ownership_operation = {
804
+ id: opId,
805
+ scheduled: isPending || isDone,
806
+ pending: isPending,
807
+ ready: isReady,
808
+ done: isDone,
809
+ execute_after_unix: tsNum > 1 ? tsNum : null,
810
+ execute_after_iso: tsNum > 1 ? new Date(tsNum * 1000).toISOString() : null,
811
+ };
812
+ }
813
+ catch (err) {
814
+ pendingHandover.accept_ownership_operation = { id: opId, error: err.message };
815
+ }
775
816
  }
776
- : null;
817
+ }
777
818
  return toolResult({
778
- gblin_v6: GBLIN_V6,
819
+ vault: GBLIN_VAULT,
820
+ lens: GBLIN_LENS,
779
821
  owner: ownerNorm,
780
822
  owner_is_timelock: ownerIsTimelock,
781
823
  owner_is_renounced: ownerIsRenounced,
782
- founder_wallet: getAddress(founder),
824
+ fee_recipient: getAddress(feeRecipient),
783
825
  trust_summary: ownerIsRenounced
784
826
  ? "Ownership fully renounced — no admin can touch the contract."
785
827
  : ownerIsTimelock
786
828
  ? "Ownership held by the 48h Timelock. All admin actions are delay-enforced on-chain."
787
- : "WARNING: owner is an EOA / unknown contract — admin actions are NOT timelocked.",
829
+ : handoverPending && pendingOwnerNorm === timelockNorm
830
+ ? "Ownership is held by the deployer with the 48h Timelock as pending owner; see pending_handover for the scheduled acceptance. Until it executes, admin actions are NOT timelocked."
831
+ : "WARNING: owner is an EOA / unknown contract — admin actions are NOT timelocked.",
788
832
  timelock: timelockState,
789
- pending_asset_proposal: pendingAssetProposal,
833
+ pending_handover: pendingHandover,
790
834
  pending_timelock_operation: operationStatus,
791
835
  verification: {
792
- gblin_v6_basescan: `https://basescan.org/address/${GBLIN_V6}#readContract`,
836
+ vault_basescan: `https://basescan.org/address/${GBLIN_VAULT}#readContract`,
837
+ lens_basescan: `https://basescan.org/address/${GBLIN_LENS}#readContract`,
793
838
  timelock_basescan: `https://basescan.org/address/${GBLIN_TIMELOCK}#readContract`,
794
- ownership_transfer_tx: "https://basescan.org/tx/0xb653f54ffa9b1764b41932e6a411077e7e34550605303f15d90900de682edaaf",
795
839
  },
796
840
  });
797
841
  }
@@ -885,12 +929,14 @@ async function handleShareSkillWithPeer(args) {
885
929
  "analyze_treasury_health",
886
930
  "get_governance_state",
887
931
  "share_skill_with_peer",
888
- "find_keeper_bounty",
932
+ "get_auction_state",
889
933
  "get_market_risk_regime",
890
934
  "verify_risk_attestation",
891
935
  ],
892
936
  chain: "Base mainnet (8453)",
893
- contract: GBLIN_V6,
937
+ vault: GBLIN_VAULT,
938
+ lens: GBLIN_LENS,
939
+ zap: GBLIN_ZAP,
894
940
  timelock: "0x6aBeC8716fFeEcf7C3D6e68255b4797113E8e5Dd",
895
941
  },
896
942
  recommended_pattern: {
@@ -906,19 +952,18 @@ async function handleShareSkillWithPeer(args) {
906
952
  amount_usdc: example_amount_usdc,
907
953
  steps: [
908
954
  "1. Call quote_safe_swap with action=buy and the desired USDC amount",
909
- "2. Call invest_usdc_to_gblin to receive 2-step unsigned calldata (approve USDC → buyGBLINInKind: direct in-kind buy, no swap)",
955
+ "2. Call invest_usdc_to_gblin to receive 2-step unsigned calldata (approve USDC to the Zap, then one call that swaps and mints at NAV)",
910
956
  "3. Sign and broadcast the 2 transactions atomically via send_calls (Base MCP) or individually",
911
- "4. When liquidity is needed, call swap_gblin_to_usdc_jit for atomic GBLIN→USDC redemption",
957
+ "4. When liquidity is needed, call swap_gblin_to_usdc_jit for the GBLIN→USDC exit (three transactions, batchable)",
912
958
  ],
913
- verified_on_mainnet: true,
914
- reference_tx: "https://basescan.org/tx/0x3d1084f6374dff2125ba337208c4b547d9821523a0a51f797f9fb422c9b24e2c",
959
+ verified_source: `https://basescan.org/address/${GBLIN_VAULT}#code`,
915
960
  },
916
961
  natural_language_explanation: [
917
962
  "GBLIN is a treasury-backed index token on Base mainnet, collateralized by WETH, cbBTC, and USDC.",
918
963
  "Every GBLIN is backed by real on-chain assets — NAV is computed from Chainlink oracle prices, not from market speculation.",
919
964
  "Holding GBLIN as treasury preserves agent capital while keeping liquidity for x402 micropayments via Just-In-Time redemption.",
920
965
  "The protocol has zero pre-mint (fair launch), 48-hour governance timelock, and an algorithmic Crash Shield that adaptively reduces risk exposure as basket assets draw down (V6 adaptive threshold, from ~15%).",
921
- "Installing this MCP server gives your AI agent 8 production-tested tools to manage treasury, quote swaps, redeem just-in-time, propagate the skill to other agents, and earn passive ETH income as a keeper.",
966
+ "Installing this MCP server gives your AI agent 13 tools to manage treasury, quote swaps, redeem just-in-time, propagate the skill to other agents, and earn passive ETH income as a keeper.",
922
967
  ].join(" "),
923
968
  referral_disclosure: {
924
969
  builder_code_standard: "ERC-8021",
@@ -957,27 +1002,22 @@ async function handleShareSkillWithPeer(args) {
957
1002
  return toolResult({ text: responseText });
958
1003
  }
959
1004
  // ───────────────────────────────────────────────────────────────────────────
960
- // TOOL 8: find_keeper_bounty
1005
+ // TOOL 8: get_auction_state
961
1006
  // ───────────────────────────────────────────────────────────────────────────
962
- const FIND_KEEPER_BOUNTY_DEFINITION = {
963
- name: "find_keeper_bounty",
964
- description: "Check if there is a profitable rebalance opportunity on GBLIN right now. GBLIN pays the caller an on-chain bounty for rebalancing its treasury when it drifts: 0.05% of the rebalanced ETH value (up to double with recent volume), floor 0.00005 ETH, cap 0.01 ETH, at most once per hour and only while the stability fund covers it — the tool reads these rules from the contract at call time. The swap uses the contract's own funds; the caller only pays gas. Returns ready-to-send calldata, the estimated reward, and whether the reward is actually payable right now (rewardGate). Use this when an AI agent wants to earn passive ETH income as a keeper on Base. Costs $0.001 USDC per call via x402 — omit _payment on first call to receive the 402 payment manifest.",
1007
+ const GET_AUCTION_STATE_DEFINITION = {
1008
+ name: "get_auction_state",
1009
+ description: "Read the GBLIN rebalancing auction on Base. The vault does not rebalance itself and pays nobody to do it: when a basket row drifts past its band it holds a Dutch auction, and whoever trades with it toward the target weights is the counterparty, at the oracle price adjusted by a premium that starts at a discount and rises to a cap over one ramp. Returns, per row, the side the vault takes, the gap in ETH, the token and amount the bidder hands over, and unsigned calldata (approval + bid). The premium is the whole reward; nothing is paid out of the vault.",
965
1010
  inputSchema: {
966
1011
  type: "object",
967
- properties: {
968
- _payment: {
969
- type: "string",
970
- description: "Base64-encoded x402 PaymentProof JSON. Omit on first call to receive the 402 payment manifest.",
971
- },
972
- },
1012
+ properties: {},
973
1013
  required: [],
974
1014
  additionalProperties: false,
975
1015
  },
976
1016
  };
977
- async function handleFindKeeperBounty() {
1017
+ export async function handleGetAuctionState() {
978
1018
  try {
979
- const bounty = await findKeeperBounty(process.env.GBLIN_RPC_URL);
980
- return toolResult(bounty);
1019
+ const state = await getAuctionState();
1020
+ return toolResult(state);
981
1021
  }
982
1022
  catch (err) {
983
1023
  return toolError(err.message, "Check RPC connectivity.");
@@ -1041,30 +1081,18 @@ async function handleMarketRiskRegime() {
1041
1081
  defensive_cash_pct: defensiveCashPct,
1042
1082
  assets,
1043
1083
  source: "GBLIN on-chain Crash Shield (Base mainnet, Chainlink-oracle drawdown)",
1044
- verify: "https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0",
1045
- meta: { contract: GBLIN_V6, chain: "base", chain_id: 8453 },
1084
+ verify: `https://basescan.org/address/${GBLIN_VAULT}#readContract`,
1085
+ meta: { vault: GBLIN_VAULT, lens: GBLIN_LENS, chain: "base", chain_id: 8453 },
1046
1086
  });
1047
1087
  }
1048
1088
  catch (err) {
1049
1089
  return toolError(err.message, "Check RPC connectivity and oracle freshness.");
1050
1090
  }
1051
1091
  }
1052
- // ───────────────────────────────────────────────────────────────────────────
1053
- // TOOL 10 — verify_risk_attestation (FREE public-good verifier — adoption flywheel)
1054
- //
1055
- // The counterpart to the paid /api/x402/attestation endpoint. Verifying a peer's
1056
- // attestation is FREE on purpose: every agent that verifies one is exposed to
1057
- // GBLIN and learns it can mint its own → drives paid calls to the attestation
1058
- // endpoint. Keyless integrity + EIP-712 recover + live-drift, no RPC secrets.
1059
- // ───────────────────────────────────────────────────────────────────────────
1060
- // EIP-712 schema — MUST stay byte-for-byte identical to the webapp signer
1061
- // (GBLIN_WEBAPP/src/app/api/x402/attestation/route.ts). Do not reorder fields.
1062
- const ATTESTATION_EIP712_DOMAIN = {
1063
- name: "GBLIN Risk Attestation",
1064
- version: "1",
1065
- chainId: 8453,
1066
- verifyingContract: GBLIN_V6,
1067
- };
1092
+ const ATTESTATION_DOMAIN_V1 = { name: "GBLIN Risk Attestation", version: "1", chainId: 8453, verifyingContract: GBLIN_PREVIOUS };
1093
+ const ATTESTATION_DOMAIN_V2 = { name: "GBLIN Risk Attestation", version: "2", chainId: 8453, verifyingContract: GBLIN_VAULT };
1094
+ const ATTESTATION_EIP712_DOMAINS = { "1": ATTESTATION_DOMAIN_V1, "2": ATTESTATION_DOMAIN_V2 };
1095
+ const ATTESTATION_EIP712_DOMAIN = ATTESTATION_DOMAIN_V2;
1068
1096
  const ATTESTATION_EIP712_TYPES = {
1069
1097
  RiskAttestation: [
1070
1098
  { name: "regime", type: "uint8" },
@@ -1117,6 +1145,8 @@ async function handleVerifyRiskAttestation(args) {
1117
1145
  if (!eip712 || !eip712.message || !eip712.domain || !eip712.types) {
1118
1146
  return toolError("Attestation is missing its `eip712` payload (domain/types/message).", "Pass the whole object returned by /api/x402/attestation, including the `eip712` field.");
1119
1147
  }
1148
+ const declaredVersion = String(eip712.domain?.version ?? "");
1149
+ const canonicalDomain = ATTESTATION_EIP712_DOMAINS[declaredVersion] ?? ATTESTATION_EIP712_DOMAIN;
1120
1150
  // Rebuild the typed message (uint64 fields as bigint for viem).
1121
1151
  const m = eip712.message;
1122
1152
  let message;
@@ -1137,7 +1167,7 @@ async function handleVerifyRiskAttestation(args) {
1137
1167
  // 1. INTEGRITY — recompute the id using the CANONICAL schema from this build
1138
1168
  // (never trust the schema embedded in the payload for hashing).
1139
1169
  const recomputedId = hashTypedData({
1140
- domain: ATTESTATION_EIP712_DOMAIN,
1170
+ domain: canonicalDomain,
1141
1171
  types: ATTESTATION_EIP712_TYPES,
1142
1172
  primaryType: "RiskAttestation",
1143
1173
  message,
@@ -1146,9 +1176,10 @@ async function handleVerifyRiskAttestation(args) {
1146
1176
  const integrityOk = claimedId ? recomputedId.toLowerCase() === claimedId.toLowerCase() : null;
1147
1177
  // The payload's OWN domain must match the canonical GBLIN domain, else it is
1148
1178
  // a different (untrusted) schema even if internally consistent.
1149
- const schemaMatches = eip712.domain?.name === ATTESTATION_EIP712_DOMAIN.name &&
1150
- String(eip712.domain?.version) === ATTESTATION_EIP712_DOMAIN.version &&
1151
- Number(eip712.domain?.chainId) === ATTESTATION_EIP712_DOMAIN.chainId;
1179
+ const schemaMatches = eip712.domain?.name === canonicalDomain.name &&
1180
+ String(eip712.domain?.version) === canonicalDomain.version &&
1181
+ Number(eip712.domain?.chainId) === canonicalDomain.chainId &&
1182
+ String(eip712.domain?.verifyingContract ?? "").toLowerCase() === canonicalDomain.verifyingContract.toLowerCase();
1152
1183
  // 2. AUTHENTICITY — signature recover
1153
1184
  const expectedAttestor = typeof a.expected_attestor === "string" && isAddress(a.expected_attestor)
1154
1185
  ? getAddress(a.expected_attestor)
@@ -1160,7 +1191,7 @@ async function handleVerifyRiskAttestation(args) {
1160
1191
  if (signature) {
1161
1192
  try {
1162
1193
  recoveredSigner = await recoverTypedDataAddress({
1163
- domain: ATTESTATION_EIP712_DOMAIN,
1194
+ domain: canonicalDomain,
1164
1195
  types: ATTESTATION_EIP712_TYPES,
1165
1196
  primaryType: "RiskAttestation",
1166
1197
  message,
@@ -1249,6 +1280,11 @@ async function handleVerifyRiskAttestation(args) {
1249
1280
  // ───────────────────────────────────────────────────────────────────────────
1250
1281
  // REGISTRY
1251
1282
  // ───────────────────────────────────────────────────────────────────────────
1283
+ // MCP tool annotations. Every tool here reads the chain or does pure math and returns data or
1284
+ // unsigned calldata: nothing is written, signed or broadcast, so each one is read-only, safe to
1285
+ // repeat with the same arguments, and reaches an open world (Base mainnet through the configured RPC).
1286
+ // The receipt tools declare their own: sealing appends to a public log.
1287
+ const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, idempotentHint: true, destructiveHint: false, openWorldHint: true };
1252
1288
  export const TOOL_DEFINITIONS = [
1253
1289
  GET_TREASURY_STATE_DEFINITION,
1254
1290
  QUOTE_SAFE_SWAP_DEFINITION,
@@ -1257,15 +1293,14 @@ export const TOOL_DEFINITIONS = [
1257
1293
  ANALYZE_TREASURY_DEFINITION,
1258
1294
  GET_GOVERNANCE_STATE_DEFINITION,
1259
1295
  SHARE_SKILL_DEFINITION,
1260
- FIND_KEEPER_BOUNTY_DEFINITION,
1296
+ GET_AUCTION_STATE_DEFINITION,
1261
1297
  MARKET_RISK_DEFINITION,
1262
1298
  VERIFY_ATTESTATION_DEFINITION,
1263
- ...RECEIPT_TOOL_DEFINITIONS,
1264
- ];
1299
+ ].map((definition) => ({ ...definition, annotations: READ_ONLY_ANNOTATIONS })).concat(RECEIPT_TOOL_DEFINITIONS);
1265
1300
  export const TOOL_HANDLERS = {
1266
1301
  // ── FREE tools ─────────────────────────────────────────────────────────────
1267
1302
  // Read-only (funnel) + action tools kept free to avoid chicken-and-egg.
1268
- // On-chain founder fee (0.05%) captures revenue from every swap/buy.
1303
+ // The on-chain protocol fee (0.05% of every mint, paid as shares) captures revenue when GBLIN is used.
1269
1304
  get_treasury_state: handleGetTreasuryState,
1270
1305
  quote_safe_swap: handleQuoteSafeSwap,
1271
1306
  get_governance_state: handleGetGovernanceState,
@@ -1273,10 +1308,10 @@ export const TOOL_HANDLERS = {
1273
1308
  swap_gblin_to_usdc_jit: handleJitSwap,
1274
1309
  invest_usdc_to_gblin: handleInvest,
1275
1310
  verify_risk_attestation: handleVerifyRiskAttestation,
1311
+ get_auction_state: handleGetAuctionState,
1276
1312
  // ── PAID tools (x402 intelligence layer) ──────────────────────────────────
1277
- // Analysis and keeper discovery — these are "advice", not transport.
1313
+ // Analysis — "advice", not transport.
1278
1314
  analyze_treasury_health: requirePayment({ priceUsdc: "0.003", priceLabel: "$0.003 USDC per call" }, handleAnalyzeTreasury),
1279
- find_keeper_bounty: requirePayment({ priceUsdc: "0.001", priceLabel: "$0.001 USDC per call" }, handleFindKeeperBounty),
1280
1315
  get_market_risk_regime: requirePayment({ priceUsdc: "0.002", priceLabel: "$0.002 USDC per call" }, handleMarketRiskRegime),
1281
1316
  // ── AI Action Receipts (free demo + free reads; paid seals via x402 HTTP) ──
1282
1317
  ...RECEIPT_TOOL_HANDLERS,