From c7dd57141befff7f25132b6274747d5d8cb534c9 Mon Sep 17 00:00:00 2001 From: GBLIN Date: Sat, 18 Jul 2026 15:03:00 +0200 Subject: [PATCH 01/14] feat(action-providers): add GBLIN treasury index (Base) --- .../src/action-providers/gblin/constants.ts | 50 ++++ .../gblin/gblinActionProvider.ts | 215 ++++++++++++++++++ .../src/action-providers/gblin/index.ts | 2 + .../src/action-providers/gblin/schemas.ts | 47 ++++ 4 files changed, 314 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/gblin/constants.ts create mode 100644 typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/gblin/index.ts create mode 100644 typescript/agentkit/src/action-providers/gblin/schemas.ts diff --git a/typescript/agentkit/src/action-providers/gblin/constants.ts b/typescript/agentkit/src/action-providers/gblin/constants.ts new file mode 100644 index 000000000..8d02210f0 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/constants.ts @@ -0,0 +1,50 @@ +export const GBLIN_ADDRESS = "0x36C81d7E1966310F305eA637e761Cf77F90852f0"; + +/** + * Minimal ABI for the GBLIN V6 index (Global Balanced Liquidity Index) on Base. + * Verified on Basescan: https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0#code + */ +export const GBLIN_ABI = [ + { + type: "function", + name: "buyGBLIN", + stateMutability: "payable", + inputs: [{ name: "minGblinOut", type: "uint256" }], + outputs: [], + }, + { + type: "function", + name: "sellGBLINForEth", + stateMutability: "nonpayable", + inputs: [ + { name: "gblinAmount", type: "uint256" }, + { name: "minEthOut", type: "uint256" }, + ], + outputs: [], + }, + { + type: "function", + name: "quoteBuyGBLIN", + stateMutability: "view", + inputs: [{ name: "ethAmount", type: "uint256" }], + outputs: [ + { name: "gblinOut", type: "uint256" }, + { name: "founderFee", type: "uint256" }, + { name: "stabilityFee", type: "uint256" }, + ], + }, + { + type: "function", + name: "quoteSellGBLIN", + stateMutability: "view", + inputs: [{ name: "gblinAmount", type: "uint256" }], + outputs: [{ name: "ethOut", type: "uint256" }], + }, + { + type: "function", + name: "totalSupply", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts new file mode 100644 index 000000000..428f991ab --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts @@ -0,0 +1,215 @@ +import { z } from "zod"; +import { Decimal } from "decimal.js"; +import { encodeFunctionData, formatEther, formatUnits, Hex, parseEther, parseUnits } from "viem"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { GBLIN_ADDRESS, GBLIN_ABI } from "./constants"; +import { BuyGblinSchema, SellGblinForEthSchema, GetTreasuryStateSchema } from "./schemas"; + +export const SUPPORTED_NETWORKS = ["base-mainnet"]; + +const DEFAULT_SLIPPAGE_BPS = 100n; // 1% +const BPS = 10_000n; + +/** + * GblinActionProvider lets an agent hold and redeem GBLIN — the collateral-backed, + * self-defending treasury index on Base (WETH/cbBTC/USDC with an autonomous Crash + * Shield). It is designed for parking surplus agent capital with capped drawdown, + * not as a USDC substitute. All actions set an on-chain-quote-derived minOut, so + * the agent is never exposed to an unbounded swap. + * + * Contract (verified): https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0 + */ +export class GblinActionProvider extends ActionProvider { + /** + * Constructor for the GblinActionProvider class. + */ + constructor() { + super("gblin", []); + } + + /** + * Buys GBLIN with ETH, protected by a min-out derived from the on-chain quote. + * + * @param wallet - The wallet instance to execute the transaction + * @param args - The input arguments for the action + * @returns A success message with transaction details or an error message + */ + @CreateAction({ + name: "buy_gblin", + description: ` +Buy GBLIN, a collateral-backed, self-defending treasury index on Base, using ETH. +Use this to park surplus agent capital with capped drawdown (managed crypto exposure, NOT a stablecoin). + +It takes: +- ethAmount: amount of ETH to spend in whole units (e.g. "0.1") +- slippageBps: optional max slippage vs the on-chain quote, in basis points (default 100 = 1%) + +The action reads quoteBuyGBLIN on-chain and submits buyGBLIN with a safe minimum output; it never sends an unbounded swap. +`, + schema: BuyGblinSchema, + }) + async buyGblin(wallet: EvmWalletProvider, args: z.infer): Promise { + const eth = new Decimal(args.ethAmount); + if (eth.comparedTo(new Decimal(0)) != 1) { + return "Error: ethAmount must be greater than 0"; + } + + try { + const valueWei = parseEther(args.ethAmount); + const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; + + const quote = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "quoteBuyGBLIN", + args: [valueWei], + })) as readonly [bigint, bigint, bigint]; + + const expectedOut = quote[0]; + if (expectedOut <= 0n) { + return "Error: on-chain quote returned zero (oracles may be stale). Try again shortly."; + } + const minOut = (expectedOut * (BPS - slippage)) / BPS; + + const data = encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "buyGBLIN", + args: [minOut], + }); + + const txHash = await wallet.sendTransaction({ + to: GBLIN_ADDRESS as `0x${string}`, + data, + value: valueWei, + }); + const receipt = await wallet.waitForTransactionReceipt(txHash); + + return `Bought GBLIN with ${args.ethAmount} ETH (min out ${formatUnits(minOut, 18)} GBLIN, expected ${formatUnits(expectedOut, 18)}). Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; + } catch (error) { + return `Error buying GBLIN: ${error}`; + } + } + + /** + * Redeems GBLIN back to ETH, protected by a min-out derived from the on-chain quote. + * + * @param wallet - The wallet instance to execute the transaction + * @param args - The input arguments for the action + * @returns A success message with transaction details or an error message + */ + @CreateAction({ + name: "sell_gblin_for_eth", + description: ` +Redeem GBLIN back to ETH (e.g. to free capital for an x402 payment). + +It takes: +- gblinAmount: amount of GBLIN to redeem in whole units (e.g. "5") +- slippageBps: optional max slippage vs the on-chain quote, in basis points (default 100 = 1%) + +Note: GBLIN enforces a short post-purchase cooldown before redemption; if you just bought, wait a couple of minutes. +`, + schema: SellGblinForEthSchema, + }) + async sellGblinForEth( + wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + const amount = new Decimal(args.gblinAmount); + if (amount.comparedTo(new Decimal(0)) != 1) { + return "Error: gblinAmount must be greater than 0"; + } + + try { + const atomic = parseUnits(args.gblinAmount, 18); + const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; + + const expectedEth = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "quoteSellGBLIN", + args: [atomic], + })) as bigint; + + if (expectedEth <= 0n) { + return "Error: on-chain quote returned zero (oracles may be stale). Try again shortly."; + } + const minEthOut = (expectedEth * (BPS - slippage)) / BPS; + + const data = encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "sellGBLINForEth", + args: [atomic, minEthOut], + }); + + const txHash = await wallet.sendTransaction({ + to: GBLIN_ADDRESS as `0x${string}`, + data, + }); + const receipt = await wallet.waitForTransactionReceipt(txHash); + + return `Redeemed ${args.gblinAmount} GBLIN for ~${formatEther(minEthOut)} ETH (min). Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; + } catch (error) { + return `Error redeeming GBLIN: ${error}`; + } + } + + /** + * Reads live GBLIN treasury state (per-unit ETH value and supply). + * + * @param wallet - The wallet instance used for on-chain reads + * @param _ - Empty args + * @returns A JSON string with treasury state or an error message + */ + @CreateAction({ + name: "get_gblin_state", + description: + "Read live GBLIN state: per-GBLIN ETH redemption value (from quoteSellGBLIN of 1 GBLIN) and total supply. Use before buying/redeeming to make an informed decision.", + schema: GetTreasuryStateSchema, + }) + async getGblinState( + wallet: EvmWalletProvider, + _: z.infer, + ): Promise { + try { + const oneGblin = parseUnits("1", 18); + const [ethPerGblin, supply] = await Promise.all([ + wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "quoteSellGBLIN", + args: [oneGblin], + }) as Promise, + wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "totalSupply", + args: [], + }) as Promise, + ]); + + return JSON.stringify({ + contract: GBLIN_ADDRESS, + network: "base-mainnet", + ethValuePerGblin: formatEther(ethPerGblin), + totalSupply: formatUnits(supply, 18), + note: "Managed crypto exposure with an autonomous Crash Shield; capped drawdown, not a stablecoin.", + }); + } catch (error) { + return `Error reading GBLIN state: ${error}`; + } + } + + /** + * Checks if the GBLIN action provider supports the given network. + * + * @param network - The network to check. + * @returns True if supported (Base mainnet), false otherwise. + */ + supportsNetwork = (network: Network) => + network.protocolFamily === "evm" && SUPPORTED_NETWORKS.includes(network.networkId!); +} + +export const gblinActionProvider = () => new GblinActionProvider(); diff --git a/typescript/agentkit/src/action-providers/gblin/index.ts b/typescript/agentkit/src/action-providers/gblin/index.ts new file mode 100644 index 000000000..25157ec30 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/index.ts @@ -0,0 +1,2 @@ +export * from "./gblinActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/gblin/schemas.ts b/typescript/agentkit/src/action-providers/gblin/schemas.ts new file mode 100644 index 000000000..c5c25caee --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +/** + * Input schema for buying GBLIN with ETH. + */ +export const BuyGblinSchema = z + .object({ + ethAmount: z + .string() + .describe("Amount of ETH to spend, in whole units (e.g. '0.1' for 0.1 ETH)"), + slippageBps: z + .number() + .int() + .min(0) + .max(2000) + .optional() + .describe("Max slippage in basis points applied to the on-chain quote. Default 100 (1%)."), + }) + .strip() + .describe("Instructions for buying GBLIN with ETH"); + +/** + * Input schema for redeeming GBLIN back to ETH. + */ +export const SellGblinForEthSchema = z + .object({ + gblinAmount: z + .string() + .describe("Amount of GBLIN to redeem, in whole units (e.g. '5' for 5 GBLIN)"), + slippageBps: z + .number() + .int() + .min(0) + .max(2000) + .optional() + .describe("Max slippage in basis points applied to the on-chain quote. Default 100 (1%)."), + }) + .strip() + .describe("Instructions for redeeming GBLIN back to ETH"); + +/** + * Input schema for reading GBLIN treasury state (no arguments). + */ +export const GetTreasuryStateSchema = z + .object({}) + .strip() + .describe("Read live GBLIN treasury state (NAV, supply, Crash Shield status)"); From 0354e57863dba9cf34d059f0f217f42cdcf4c8ae Mon Sep 17 00:00:00 2001 From: GBLIN Date: Sat, 18 Jul 2026 15:36:14 +0200 Subject: [PATCH 02/14] test+docs: add GBLIN provider tests and README --- .../src/action-providers/gblin/README.md | 38 ++++++ .../gblin/gblinActionProvider.test.ts | 120 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/gblin/README.md create mode 100644 typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts diff --git a/typescript/agentkit/src/action-providers/gblin/README.md b/typescript/agentkit/src/action-providers/gblin/README.md new file mode 100644 index 000000000..0745dbf58 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/README.md @@ -0,0 +1,38 @@ +# GBLIN Action Provider + +Actions for interacting with [GBLIN](https://gblin.digital), a collateral-backed, +self-defending treasury index on Base (WETH / cbBTC / USDC) with an autonomous +on-chain "Crash Shield" that reduces risk during drawdowns. It is intended for +parking surplus agent capital with capped drawdown — managed crypto exposure, +**not** a stablecoin and not financial advice. + +Contract (verified): [`0x36C81d7E1966310F305eA637e761Cf77F90852f0`](https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0#code) + +## Actions + +| Action | Description | +| --- | --- | +| `buy_gblin` | Buy GBLIN with ETH. Reads `quoteBuyGBLIN` on-chain and submits `buyGBLIN` with a slippage-bounded minimum output. | +| `sell_gblin_for_eth` | Redeem GBLIN back to ETH (e.g. to fund an x402 payment). Reads `quoteSellGBLIN` and submits `sellGBLINForEth` with a min-out. | +| `get_gblin_state` | Read per-GBLIN ETH redemption value and total supply. | + +Every state-changing action derives its minimum output from the contract's own +quote function, so the agent is never exposed to an unbounded swap. + +## Network support + +Base mainnet (`base-mainnet`) only. + +## Example + +```typescript +import { gblinActionProvider } from "@coinbase/agentkit"; + +const provider = gblinActionProvider(); +``` + +## Notes + +- GBLIN enforces a short post-purchase cooldown before redemption. +- The risk policy is public code governed by a 48h timelock, and has executed + autonomously on mainnet ([activation tx](https://basescan.org/tx/0x896be221989930776972c78f81e2be9081c90d0027c14f7cd74bf51b9ad0acca)). diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts new file mode 100644 index 000000000..65241839b --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts @@ -0,0 +1,120 @@ +import { encodeFunctionData, parseEther, parseUnits } from "viem"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { GblinActionProvider } from "./gblinActionProvider"; +import { GBLIN_ADDRESS, GBLIN_ABI } from "./constants"; + +const MOCK_TX_HASH = "0xabcdef1234567890"; +const MOCK_RECEIPT = { status: 1, blockNumber: 1234567 }; +const BPS = 10_000n; +const DEFAULT_SLIPPAGE_BPS = 100n; + +describe("GBLIN Action Provider", () => { + const actionProvider = new GblinActionProvider(); + let mockWallet: jest.Mocked; + + beforeEach(() => { + mockWallet = { + getAddress: jest.fn().mockReturnValue("0x9876543210987654321098765432109876543210"), + getNetwork: jest.fn().mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }), + sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH as `0x${string}`), + waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT), + readContract: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("buyGblin", () => { + it("should buy GBLIN with ETH using a quote-derived minOut", async () => { + const expectedOut = parseUnits("30", 18); + const minOut = (expectedOut * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; + mockWallet.readContract.mockResolvedValueOnce([expectedOut, 0n, 0n]); + + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "buyGBLIN", + args: [minOut], + }), + value: parseEther("0.1"), + }); + expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should reject a non-positive amount", async () => { + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0" }); + expect(response).toContain("must be greater than 0"); + }); + + it("should handle a zero on-chain quote (stale oracles)", async () => { + mockWallet.readContract.mockResolvedValueOnce([0n, 0n, 0n]); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("zero"); + }); + }); + + describe("sellGblinForEth", () => { + it("should redeem GBLIN for ETH using a quote-derived minOut", async () => { + const atomic = parseUnits("5", 18); + const expectedEth = parseEther("0.02"); + const minEthOut = (expectedEth * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; + mockWallet.readContract.mockResolvedValueOnce(expectedEth); + + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "sellGBLINForEth", + args: [atomic, minEthOut], + }), + }); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle errors when redeeming", async () => { + mockWallet.readContract.mockResolvedValueOnce(parseEther("0.02")); + mockWallet.sendTransaction.mockRejectedValue(new Error("Failed")); + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + expect(response).toContain("Error redeeming GBLIN"); + }); + }); + + describe("getGblinState", () => { + it("should return live state as JSON", async () => { + mockWallet.readContract + .mockResolvedValueOnce(parseEther("0.0012")) // quoteSellGBLIN(1) + .mockResolvedValueOnce(parseUnits("0.5", 18)); // totalSupply + + const response = await actionProvider.getGblinState(mockWallet, {}); + const parsed = JSON.parse(response); + expect(parsed.contract).toBe(GBLIN_ADDRESS); + expect(parsed.network).toBe("base-mainnet"); + expect(parsed).toHaveProperty("ethValuePerGblin"); + expect(parsed).toHaveProperty("totalSupply"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for Base Mainnet", () => { + expect( + actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet" }), + ).toBe(true); + }); + + it("should return false for other EVM networks", () => { + expect( + actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum" }), + ).toBe(false); + }); + + it("should return false for non-EVM networks", () => { + expect( + actionProvider.supportsNetwork({ protocolFamily: "bitcoin", networkId: "base-mainnet" }), + ).toBe(false); + }); + }); +}); From 0dda5acd7303a9fc7b0c8c76c511ffe70a329dcc Mon Sep 17 00:00:00 2001 From: GBLIN Date: Sat, 18 Jul 2026 15:39:14 +0200 Subject: [PATCH 03/14] chore: add changeset --- .changeset/gblin-action-provider.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gblin-action-provider.md diff --git a/.changeset/gblin-action-provider.md b/.changeset/gblin-action-provider.md new file mode 100644 index 000000000..b80971887 --- /dev/null +++ b/.changeset/gblin-action-provider.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added a GBLIN action provider to buy, redeem, and read GBLIN — a collateral-backed treasury index with an on-chain crash shield on Base. From 5df3b3dadbde9dca09ed446fc30224cb73ae4979 Mon Sep 17 00:00:00 2001 From: GBLIN Date: Sat, 18 Jul 2026 15:46:14 +0200 Subject: [PATCH 04/14] chore: register gblin action provider export --- typescript/agentkit/src/action-providers/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..d6088292f 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -17,6 +17,7 @@ export * from "./erc20"; export * from "./erc721"; export * from "./erc8004"; export * from "./farcaster"; +export * from "./gblin"; export * from "./jupiter"; export * from "./messari"; export * from "./pyth"; From 3b6aac7bb29c9f6072028a58fc762bd694ac5182 Mon Sep 17 00:00:00 2001 From: GBLIN Date: Sat, 18 Jul 2026 15:51:14 +0200 Subject: [PATCH 05/14] docs: add GBLIN to the action providers list --- typescript/agentkit/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 37b14207f..47e4c18e1 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -485,6 +485,23 @@ const agent = createAgent({
+GBLIN + + + + + + + + + + + + + +
buy_gblinBuys GBLIN, a collateral-backed treasury index on Base, with ETH. Derives a slippage-bounded minimum output from the on-chain quote.
sell_gblin_for_ethRedeems GBLIN back to ETH. Derives a slippage-bounded minimum output from the on-chain quote.
get_gblin_stateReads the live per-GBLIN ETH redemption value and total supply.
+
+
Messari From 20754d4c2cfc8d17c5b8da77846a1764121d5fbc Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:25:30 +0200 Subject: [PATCH 06/14] gblin: describe the provider for the vault in service --- .changeset/gblin-action-provider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/gblin-action-provider.md b/.changeset/gblin-action-provider.md index b80971887..8149f5132 100644 --- a/.changeset/gblin-action-provider.md +++ b/.changeset/gblin-action-provider.md @@ -2,4 +2,4 @@ "@coinbase/agentkit": patch --- -Added a GBLIN action provider to buy, redeem, and read GBLIN — a collateral-backed treasury index with an on-chain crash shield on Base. +Added a GBLIN action provider to buy, redeem, and read GBLIN — a collateral-backed treasury index on Base. From 809b53c5e01d44ffd1a2be3bf1a0fa29e6a61e45 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:26:05 +0200 Subject: [PATCH 07/14] gblin: describe the state schema --- typescript/agentkit/src/action-providers/gblin/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/agentkit/src/action-providers/gblin/schemas.ts b/typescript/agentkit/src/action-providers/gblin/schemas.ts index c5c25caee..ed7be81c2 100644 --- a/typescript/agentkit/src/action-providers/gblin/schemas.ts +++ b/typescript/agentkit/src/action-providers/gblin/schemas.ts @@ -44,4 +44,4 @@ export const SellGblinForEthSchema = z export const GetTreasuryStateSchema = z .object({}) .strip() - .describe("Read live GBLIN treasury state (NAV, supply, Crash Shield status)"); + .describe("Read live GBLIN state (ETH value per share, supply, NAV reliability)"); From f0c50a5012098fc1719a76645b0d4fd0fc2e2f5d Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:27:01 +0200 Subject: [PATCH 08/14] gblin: document the provider for the vault in service --- .../src/action-providers/gblin/README.md | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/typescript/agentkit/src/action-providers/gblin/README.md b/typescript/agentkit/src/action-providers/gblin/README.md index 0745dbf58..c7ac7bfb1 100644 --- a/typescript/agentkit/src/action-providers/gblin/README.md +++ b/typescript/agentkit/src/action-providers/gblin/README.md @@ -1,23 +1,26 @@ # GBLIN Action Provider -Actions for interacting with [GBLIN](https://gblin.digital), a collateral-backed, -self-defending treasury index on Base (WETH / cbBTC / USDC) with an autonomous -on-chain "Crash Shield" that reduces risk during drawdowns. It is intended for -parking surplus agent capital with capped drawdown — managed crypto exposure, -**not** a stablecoin and not financial advice. +Actions for interacting with [GBLIN](https://gblin.digital), a collateral-backed treasury index +on Base (cbBTC / WETH / USDC). The vault mints at NAV, redeems pro rata in kind and reduces the +weight of a basket asset on-chain when it draws down. It is intended for parking surplus agent +capital in managed crypto exposure — **not** a stablecoin and not financial advice. -Contract (verified): [`0x36C81d7E1966310F305eA637e761Cf77F90852f0`](https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0#code) +| Contract | Address | +| --- | --- | +| Vault (share token) | [`0xc2181d975c05c8c724b334bcED0764c0b86B1D53`](https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53#code) | +| Lens (quotes) | [`0xfCFea8027019E8551A1f09AD91532471F5D26f61`](https://basescan.org/address/0xfCFea8027019E8551A1f09AD91532471F5D26f61#code) | +| Zap (exit to ETH) | [`0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200`](https://basescan.org/address/0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200#code) | ## Actions | Action | Description | | --- | --- | -| `buy_gblin` | Buy GBLIN with ETH. Reads `quoteBuyGBLIN` on-chain and submits `buyGBLIN` with a slippage-bounded minimum output. | -| `sell_gblin_for_eth` | Redeem GBLIN back to ETH (e.g. to fund an x402 payment). Reads `quoteSellGBLIN` and submits `sellGBLINForEth` with a min-out. | -| `get_gblin_state` | Read per-GBLIN ETH redemption value and total supply. | +| `buy_gblin` | Buy GBLIN with ETH. Reads `quoteBuy` from the Lens and calls `buyGBLIN` on the vault with a slippage-bounded minimum output. | +| `sell_gblin_for_eth` | Redeem GBLIN back to ETH (e.g. to fund an x402 payment). Reads `quoteSell` from the Lens, approves the shares to the Zap if needed, and calls `GBLINZap.sellGBLINForEth`, which redeems in kind and sells every leg, all or nothing. | +| `get_gblin_state` | Read the ETH value of one GBLIN at NAV, the total supply, and whether the vault reports its NAV as reliable. | -Every state-changing action derives its minimum output from the contract's own -quote function, so the agent is never exposed to an unbounded swap. +Every state-changing action derives its minimum output from an on-chain quote, and both +refuse to trade while the vault reports its NAV as not reliable. ## Network support @@ -33,6 +36,6 @@ const provider = gblinActionProvider(); ## Notes -- GBLIN enforces a short post-purchase cooldown before redemption. -- The risk policy is public code governed by a 48h timelock, and has executed - autonomously on mainnet ([activation tx](https://basescan.org/tx/0x896be221989930776972c78f81e2be9081c90d0027c14f7cd74bf51b9ad0acca)). +- Fees: 0.10% on mint and a 0.50% yearly management fee accrued as new shares; redemption pays no protocol fee. +- The vault enforces a 20-second redemption cooldown after a mint for oneself. +- The vault is owned by a 48-hour timelock; its parameters can change through it. From 6f8b5b5b5dd868ca8546f020a62f04d9500e5340 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:28:29 +0200 Subject: [PATCH 09/14] gblin: target the vault, Lens and Zap in service --- .../src/action-providers/gblin/constants.ts | 97 +++++++++++++++---- 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/typescript/agentkit/src/action-providers/gblin/constants.ts b/typescript/agentkit/src/action-providers/gblin/constants.ts index 8d02210f0..bf2a18fa8 100644 --- a/typescript/agentkit/src/action-providers/gblin/constants.ts +++ b/typescript/agentkit/src/action-providers/gblin/constants.ts @@ -1,50 +1,113 @@ -export const GBLIN_ADDRESS = "0x36C81d7E1966310F305eA637e761Cf77F90852f0"; +/** + * GBLIN vault (Global Balanced Liquidity Index) on Base mainnet. The vault is the ERC-20 share token. + * Verified source: https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53#code + */ +export const GBLIN_ADDRESS = "0xc2181d975c05c8c724b334bcED0764c0b86B1D53"; + +/** + * GBLIN Lens: read-only quotes and state for the vault. + */ +export const GBLIN_LENS_ADDRESS = "0xfCFea8027019E8551A1f09AD91532471F5D26f61"; + +/** + * GBLIN Zap: redeems shares in kind and sells every basket leg for ETH in one transaction. + */ +export const GBLIN_ZAP_ADDRESS = "0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200"; /** - * Minimal ABI for the GBLIN V6 index (Global Balanced Liquidity Index) on Base. - * Verified on Basescan: https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0#code + * Uniswap V3 fee tier (0.05%) used to route every basket leg in the Zap exit. */ +export const VENUE_FEE_TIER = 500; + export const GBLIN_ABI = [ { type: "function", name: "buyGBLIN", stateMutability: "payable", - inputs: [{ name: "minGblinOut", type: "uint256" }], + inputs: [{ name: "minOut", type: "uint256" }], outputs: [], }, { type: "function", - name: "sellGBLINForEth", + name: "isNavReliable", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "bool" }], + }, + { + type: "function", + name: "totalSupply", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "allowance", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "approve", stateMutability: "nonpayable", inputs: [ - { name: "gblinAmount", type: "uint256" }, - { name: "minEthOut", type: "uint256" }, + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, ], - outputs: [], + outputs: [{ name: "", type: "bool" }], }, +] as const; + +export const GBLIN_LENS_ABI = [ { type: "function", - name: "quoteBuyGBLIN", + name: "quoteBuy", stateMutability: "view", - inputs: [{ name: "ethAmount", type: "uint256" }], + inputs: [ + { name: "vault", type: "address" }, + { name: "ethValue", type: "uint256" }, + ], outputs: [ - { name: "gblinOut", type: "uint256" }, - { name: "founderFee", type: "uint256" }, + { name: "out", type: "uint256" }, + { name: "protocolFee", type: "uint256" }, { name: "stabilityFee", type: "uint256" }, ], }, { type: "function", - name: "quoteSellGBLIN", + name: "quoteSell", stateMutability: "view", - inputs: [{ name: "gblinAmount", type: "uint256" }], - outputs: [{ name: "ethOut", type: "uint256" }], + inputs: [ + { name: "vault", type: "address" }, + { name: "gblinAmount", type: "uint256" }, + ], + outputs: [{ name: "", type: "uint256" }], }, { type: "function", - name: "totalSupply", + name: "basketLength", stateMutability: "view", - inputs: [], + inputs: [{ name: "vault", type: "address" }], outputs: [{ name: "", type: "uint256" }], }, ] as const; + +export const GBLIN_ZAP_ABI = [ + { + type: "function", + name: "sellGBLINForEth", + stateMutability: "nonpayable", + inputs: [ + { name: "shares", type: "uint256" }, + { name: "minEthOut", type: "uint256" }, + { name: "venueData", type: "bytes[]" }, + { name: "receiver", type: "address" }, + ], + outputs: [{ name: "ethOut", type: "uint256" }], + }, +] as const; From 961c8da30ee755def70849c70c32a485fae21a70 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:30:19 +0200 Subject: [PATCH 10/14] gblin: quote through the Lens and exit through the Zap --- .../gblin/gblinActionProvider.ts | 147 +++++++++++++----- 1 file changed, 109 insertions(+), 38 deletions(-) diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts index 428f991ab..0c6b6642c 100644 --- a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts @@ -1,26 +1,41 @@ import { z } from "zod"; import { Decimal } from "decimal.js"; -import { encodeFunctionData, formatEther, formatUnits, Hex, parseEther, parseUnits } from "viem"; +import { + encodeAbiParameters, + encodeFunctionData, + formatEther, + formatUnits, + Hex, + parseEther, + parseUnits, +} from "viem"; import { ActionProvider } from "../actionProvider"; import { EvmWalletProvider } from "../../wallet-providers"; import { CreateAction } from "../actionDecorator"; import { Network } from "../../network"; -import { GBLIN_ADDRESS, GBLIN_ABI } from "./constants"; +import { + GBLIN_ABI, + GBLIN_ADDRESS, + GBLIN_LENS_ABI, + GBLIN_LENS_ADDRESS, + GBLIN_ZAP_ABI, + GBLIN_ZAP_ADDRESS, + VENUE_FEE_TIER, +} from "./constants"; import { BuyGblinSchema, SellGblinForEthSchema, GetTreasuryStateSchema } from "./schemas"; -export const SUPPORTED_NETWORKS = ["base-mainnet"]; +const SUPPORTED_NETWORKS = ["base-mainnet"]; const DEFAULT_SLIPPAGE_BPS = 100n; // 1% const BPS = 10_000n; /** - * GblinActionProvider lets an agent hold and redeem GBLIN — the collateral-backed, - * self-defending treasury index on Base (WETH/cbBTC/USDC with an autonomous Crash - * Shield). It is designed for parking surplus agent capital with capped drawdown, - * not as a USDC substitute. All actions set an on-chain-quote-derived minOut, so - * the agent is never exposed to an unbounded swap. + * GblinActionProvider lets an agent hold and redeem GBLIN, a collateral-backed treasury index + * on Base (cbBTC, WETH and USDC) whose basket weights are reduced on-chain when an asset draws + * down. The vault mints at NAV and never swaps; the exit to ETH goes through the GBLIN Zap. + * Every state-changing action sets a minimum output derived from the on-chain quote. * - * Contract (verified): https://basescan.org/address/0x36C81d7E1966310F305eA637e761Cf77F90852f0 + * Vault (verified): https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53 */ export class GblinActionProvider extends ActionProvider { /** @@ -40,14 +55,14 @@ export class GblinActionProvider extends ActionProvider { @CreateAction({ name: "buy_gblin", description: ` -Buy GBLIN, a collateral-backed, self-defending treasury index on Base, using ETH. -Use this to park surplus agent capital with capped drawdown (managed crypto exposure, NOT a stablecoin). +Buy GBLIN, a collateral-backed treasury index on Base (cbBTC, WETH, USDC), using ETH. +Use this to park surplus agent capital in managed crypto exposure. GBLIN is NOT a stablecoin: its NAV moves with BTC and ETH. It takes: - ethAmount: amount of ETH to spend in whole units (e.g. "0.1") - slippageBps: optional max slippage vs the on-chain quote, in basis points (default 100 = 1%) -The action reads quoteBuyGBLIN on-chain and submits buyGBLIN with a safe minimum output; it never sends an unbounded swap. +The action reads the quote from the GBLIN Lens and calls buyGBLIN on the vault with a safe minimum output. The mint fee is 0.10%; a 0.50% yearly management fee accrues as new shares. `, schema: BuyGblinSchema, }) @@ -61,16 +76,26 @@ The action reads quoteBuyGBLIN on-chain and submits buyGBLIN with a safe minimum const valueWei = parseEther(args.ethAmount); const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; - const quote = (await wallet.readContract({ + const reliable = (await wallet.readContract({ address: GBLIN_ADDRESS as Hex, abi: GBLIN_ABI, - functionName: "quoteBuyGBLIN", - args: [valueWei], + functionName: "isNavReliable", + args: [], + })) as boolean; + if (!reliable) { + return "Error: the vault reports its NAV as not reliable right now (a price feed or a basket balance is unavailable). Try again later."; + } + + const quote = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteBuy", + args: [GBLIN_ADDRESS as Hex, valueWei], })) as readonly [bigint, bigint, bigint]; const expectedOut = quote[0]; if (expectedOut <= 0n) { - return "Error: on-chain quote returned zero (oracles may be stale). Try again shortly."; + return "Error: on-chain quote returned zero. Try again shortly."; } const minOut = (expectedOut * (BPS - slippage)) / BPS; @@ -94,7 +119,8 @@ The action reads quoteBuyGBLIN on-chain and submits buyGBLIN with a safe minimum } /** - * Redeems GBLIN back to ETH, protected by a min-out derived from the on-chain quote. + * Redeems GBLIN back to ETH through the GBLIN Zap, protected by a min-out derived from the + * on-chain quote. * * @param wallet - The wallet instance to execute the transaction * @param args - The input arguments for the action @@ -107,9 +133,10 @@ Redeem GBLIN back to ETH (e.g. to free capital for an x402 payment). It takes: - gblinAmount: amount of GBLIN to redeem in whole units (e.g. "5") -- slippageBps: optional max slippage vs the on-chain quote, in basis points (default 100 = 1%) +- slippageBps: optional max slippage vs the on-chain NAV quote, in basis points (default 100 = 1%) -Note: GBLIN enforces a short post-purchase cooldown before redemption; if you just bought, wait a couple of minutes. +The GBLIN Zap redeems the shares in kind and sells every basket leg for ETH, all or nothing. If the shares are not yet approved to the Zap, the action first sends an approval. +Note: the vault enforces a 20-second redemption cooldown after a mint for oneself; if you just bought, wait before selling. `, schema: SellGblinForEthSchema, }) @@ -123,50 +150,87 @@ Note: GBLIN enforces a short post-purchase cooldown before redemption; if you ju } try { - const atomic = parseUnits(args.gblinAmount, 18); + const shares = parseUnits(args.gblinAmount, 18); const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; + const owner = (await wallet.getAddress()) as Hex; - const expectedEth = (await wallet.readContract({ + const reliable = (await wallet.readContract({ address: GBLIN_ADDRESS as Hex, abi: GBLIN_ABI, - functionName: "quoteSellGBLIN", - args: [atomic], - })) as bigint; + functionName: "isNavReliable", + args: [], + })) as boolean; + if (!reliable) { + return "Error: the vault reports its NAV as not reliable right now (a price feed or a basket balance is unavailable). Try again later."; + } + const expectedEth = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteSell", + args: [GBLIN_ADDRESS as Hex, shares], + })) as bigint; if (expectedEth <= 0n) { - return "Error: on-chain quote returned zero (oracles may be stale). Try again shortly."; + return "Error: on-chain quote returned zero. Try again shortly."; } const minEthOut = (expectedEth * (BPS - slippage)) / BPS; - const data = encodeFunctionData({ + const rows = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "basketLength", + args: [GBLIN_ADDRESS as Hex], + })) as bigint; + const venue = encodeAbiParameters([{ type: "uint24" }], [VENUE_FEE_TIER]); + const venueData = Array.from({ length: Number(rows) }, () => venue); + + const allowance = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, abi: GBLIN_ABI, + functionName: "allowance", + args: [owner, GBLIN_ZAP_ADDRESS as Hex], + })) as bigint; + if (allowance < shares) { + const approveHash = await wallet.sendTransaction({ + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "approve", + args: [GBLIN_ZAP_ADDRESS as Hex, shares], + }), + }); + await wallet.waitForTransactionReceipt(approveHash); + } + + const data = encodeFunctionData({ + abi: GBLIN_ZAP_ABI, functionName: "sellGBLINForEth", - args: [atomic, minEthOut], + args: [shares, minEthOut, venueData, owner], }); const txHash = await wallet.sendTransaction({ - to: GBLIN_ADDRESS as `0x${string}`, + to: GBLIN_ZAP_ADDRESS as `0x${string}`, data, }); const receipt = await wallet.waitForTransactionReceipt(txHash); - return `Redeemed ${args.gblinAmount} GBLIN for ~${formatEther(minEthOut)} ETH (min). Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; + return `Redeemed ${args.gblinAmount} GBLIN for at least ${formatEther(minEthOut)} ETH. Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; } catch (error) { return `Error redeeming GBLIN: ${error}`; } } /** - * Reads live GBLIN treasury state (per-unit ETH value and supply). + * Reads live GBLIN state (per-share ETH value, supply and NAV reliability). * * @param wallet - The wallet instance used for on-chain reads * @param _ - Empty args - * @returns A JSON string with treasury state or an error message + * @returns A JSON string with the vault state or an error message */ @CreateAction({ name: "get_gblin_state", description: - "Read live GBLIN state: per-GBLIN ETH redemption value (from quoteSellGBLIN of 1 GBLIN) and total supply. Use before buying/redeeming to make an informed decision.", + "Read live GBLIN state: ETH value of one GBLIN at NAV (from the GBLIN Lens), total supply, and whether the vault reports its NAV as reliable. Use before buying or redeeming.", schema: GetTreasuryStateSchema, }) async getGblinState( @@ -175,12 +239,12 @@ Note: GBLIN enforces a short post-purchase cooldown before redemption; if you ju ): Promise { try { const oneGblin = parseUnits("1", 18); - const [ethPerGblin, supply] = await Promise.all([ + const [ethPerGblin, supply, navReliable] = await Promise.all([ wallet.readContract({ - address: GBLIN_ADDRESS as Hex, - abi: GBLIN_ABI, - functionName: "quoteSellGBLIN", - args: [oneGblin], + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteSell", + args: [GBLIN_ADDRESS as Hex, oneGblin], }) as Promise, wallet.readContract({ address: GBLIN_ADDRESS as Hex, @@ -188,6 +252,12 @@ Note: GBLIN enforces a short post-purchase cooldown before redemption; if you ju functionName: "totalSupply", args: [], }) as Promise, + wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "isNavReliable", + args: [], + }) as Promise, ]); return JSON.stringify({ @@ -195,7 +265,8 @@ Note: GBLIN enforces a short post-purchase cooldown before redemption; if you ju network: "base-mainnet", ethValuePerGblin: formatEther(ethPerGblin), totalSupply: formatUnits(supply, 18), - note: "Managed crypto exposure with an autonomous Crash Shield; capped drawdown, not a stablecoin.", + navReliable, + note: "Managed crypto exposure (cbBTC, WETH, USDC) with on-chain drawdown protection; not a stablecoin.", }); } catch (error) { return `Error reading GBLIN state: ${error}`; From 6a11b00ee6ce9c48e4e0beff1e0cffaf0c954f49 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:31:42 +0200 Subject: [PATCH 11/14] gblin: test the Lens quotes, the Zap exit and the NAV guard --- .../gblin/gblinActionProvider.test.ts | 114 ++++++++++++++---- 1 file changed, 90 insertions(+), 24 deletions(-) diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts index 65241839b..724d79f14 100644 --- a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts @@ -1,12 +1,20 @@ -import { encodeFunctionData, parseEther, parseUnits } from "viem"; +import { encodeAbiParameters, encodeFunctionData, parseEther, parseUnits } from "viem"; import { EvmWalletProvider } from "../../wallet-providers"; import { GblinActionProvider } from "./gblinActionProvider"; -import { GBLIN_ADDRESS, GBLIN_ABI } from "./constants"; +import { + GBLIN_ABI, + GBLIN_ADDRESS, + GBLIN_LENS_ADDRESS, + GBLIN_ZAP_ABI, + GBLIN_ZAP_ADDRESS, +} from "./constants"; const MOCK_TX_HASH = "0xabcdef1234567890"; const MOCK_RECEIPT = { status: 1, blockNumber: 1234567 }; +const MOCK_ADDRESS = "0x9876543210987654321098765432109876543210"; const BPS = 10_000n; const DEFAULT_SLIPPAGE_BPS = 100n; +const VENUE = encodeAbiParameters([{ type: "uint24" }], [500]); describe("GBLIN Action Provider", () => { const actionProvider = new GblinActionProvider(); @@ -14,7 +22,7 @@ describe("GBLIN Action Provider", () => { beforeEach(() => { mockWallet = { - getAddress: jest.fn().mockReturnValue("0x9876543210987654321098765432109876543210"), + getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS), getNetwork: jest.fn().mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }), sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH as `0x${string}`), waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT), @@ -23,13 +31,23 @@ describe("GBLIN Action Provider", () => { }); describe("buyGblin", () => { - it("should buy GBLIN with ETH using a quote-derived minOut", async () => { - const expectedOut = parseUnits("30", 18); + it("should buy GBLIN on the vault with a quote-derived minOut", async () => { + const expectedOut = parseUnits("0.03", 18); const minOut = (expectedOut * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; - mockWallet.readContract.mockResolvedValueOnce([expectedOut, 0n, 0n]); + mockWallet.readContract + .mockResolvedValueOnce(true) // isNavReliable + .mockResolvedValueOnce([expectedOut, 0n, 0n]); // Lens.quoteBuy const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(mockWallet.readContract).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + address: GBLIN_LENS_ADDRESS, + functionName: "quoteBuy", + args: [GBLIN_ADDRESS, parseEther("0.1")], + }), + ); expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ to: GBLIN_ADDRESS as `0x${string}`, data: encodeFunctionData({ @@ -48,35 +66,81 @@ describe("GBLIN Action Provider", () => { expect(response).toContain("must be greater than 0"); }); - it("should handle a zero on-chain quote (stale oracles)", async () => { - mockWallet.readContract.mockResolvedValueOnce([0n, 0n, 0n]); + it("should not buy while the NAV is not reliable", async () => { + mockWallet.readContract.mockResolvedValueOnce(false); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("not reliable"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should handle a zero on-chain quote", async () => { + mockWallet.readContract.mockResolvedValueOnce(true).mockResolvedValueOnce([0n, 0n, 0n]); const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); expect(response).toContain("zero"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); }); }); describe("sellGblinForEth", () => { - it("should redeem GBLIN for ETH using a quote-derived minOut", async () => { - const atomic = parseUnits("5", 18); - const expectedEth = parseEther("0.02"); - const minEthOut = (expectedEth * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; - mockWallet.readContract.mockResolvedValueOnce(expectedEth); + const shares = parseUnits("5", 18); + const expectedEth = parseEther("0.2"); + const minEthOut = (expectedEth * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; + const zapCall = { + to: GBLIN_ZAP_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ZAP_ABI, + functionName: "sellGBLINForEth", + args: [shares, minEthOut, [VENUE, VENUE, VENUE], MOCK_ADDRESS], + }), + }; + + it("should approve the Zap and redeem through it when the allowance is short", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) // isNavReliable + .mockResolvedValueOnce(expectedEth) // Lens.quoteSell + .mockResolvedValueOnce(3n) // Lens.basketLength + .mockResolvedValueOnce(0n); // allowance const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); - expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + expect(mockWallet.sendTransaction).toHaveBeenNthCalledWith(1, { to: GBLIN_ADDRESS as `0x${string}`, data: encodeFunctionData({ abi: GBLIN_ABI, - functionName: "sellGBLINForEth", - args: [atomic, minEthOut], + functionName: "approve", + args: [GBLIN_ZAP_ADDRESS, shares], }), }); + expect(mockWallet.sendTransaction).toHaveBeenNthCalledWith(2, zapCall); expect(response).toContain(MOCK_TX_HASH); }); + it("should skip the approval when the allowance already covers the shares", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(expectedEth) + .mockResolvedValueOnce(3n) + .mockResolvedValueOnce(shares); + + await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); + expect(mockWallet.sendTransaction).toHaveBeenCalledWith(zapCall); + }); + + it("should not sell while the NAV is not reliable", async () => { + mockWallet.readContract.mockResolvedValueOnce(false); + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + expect(response).toContain("not reliable"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); + it("should handle errors when redeeming", async () => { - mockWallet.readContract.mockResolvedValueOnce(parseEther("0.02")); + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(expectedEth) + .mockResolvedValueOnce(3n) + .mockResolvedValueOnce(shares); mockWallet.sendTransaction.mockRejectedValue(new Error("Failed")); const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); expect(response).toContain("Error redeeming GBLIN"); @@ -86,15 +150,17 @@ describe("GBLIN Action Provider", () => { describe("getGblinState", () => { it("should return live state as JSON", async () => { mockWallet.readContract - .mockResolvedValueOnce(parseEther("0.0012")) // quoteSellGBLIN(1) - .mockResolvedValueOnce(parseUnits("0.5", 18)); // totalSupply + .mockResolvedValueOnce(parseEther("0.04")) // Lens.quoteSell(1) + .mockResolvedValueOnce(parseUnits("0.5", 18)) // totalSupply + .mockResolvedValueOnce(true); // isNavReliable const response = await actionProvider.getGblinState(mockWallet, {}); const parsed = JSON.parse(response); expect(parsed.contract).toBe(GBLIN_ADDRESS); expect(parsed.network).toBe("base-mainnet"); - expect(parsed).toHaveProperty("ethValuePerGblin"); - expect(parsed).toHaveProperty("totalSupply"); + expect(parsed.ethValuePerGblin).toBe("0.04"); + expect(parsed.totalSupply).toBe("0.5"); + expect(parsed.navReliable).toBe(true); }); }); @@ -106,9 +172,9 @@ describe("GBLIN Action Provider", () => { }); it("should return false for other EVM networks", () => { - expect( - actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum" }), - ).toBe(false); + expect(actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum" })).toBe( + false, + ); }); it("should return false for non-EVM networks", () => { From fa90901242dc621be07b8dcb639c1f1a7475fe40 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 12:32:11 +0200 Subject: [PATCH 12/14] docs: describe the GBLIN actions for the vault in service --- typescript/agentkit/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 47e4c18e1..d80922078 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -493,11 +493,11 @@ const agent = createAgent({ - + - +
sell_gblin_for_ethRedeems GBLIN back to ETH. Derives a slippage-bounded minimum output from the on-chain quote.Redeems GBLIN back to ETH through the GBLIN Zap, approving the shares first if needed. Derives a slippage-bounded minimum output from the on-chain quote.
get_gblin_stateReads the live per-GBLIN ETH redemption value and total supply.Reads the ETH value of one GBLIN at NAV, the total supply, and whether the NAV is reliable.
From 525e8383e79fcfc8ebdeeeef12d1d38bd8fb9c7e Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 13:58:32 +0200 Subject: [PATCH 13/14] gblin: report the transaction hash and flag reverted transactions --- .../action-providers/gblin/gblinActionProvider.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts index 0c6b6642c..9c7de8eaa 100644 --- a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts @@ -111,8 +111,11 @@ The action reads the quote from the GBLIN Lens and calls buyGBLIN on the vault w value: valueWei, }); const receipt = await wallet.waitForTransactionReceipt(txHash); + if (receipt?.status === "reverted") { + return `Error buying GBLIN: transaction ${txHash} reverted.`; + } - return `Bought GBLIN with ${args.ethAmount} ETH (min out ${formatUnits(minOut, 18)} GBLIN, expected ${formatUnits(expectedOut, 18)}). Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; + return `Bought GBLIN with ${args.ethAmount} ETH (min out ${formatUnits(minOut, 18)} GBLIN, expected ${formatUnits(expectedOut, 18)}). Transaction hash: ${txHash}`; } catch (error) { return `Error buying GBLIN: ${error}`; } @@ -199,7 +202,10 @@ Note: the vault enforces a 20-second redemption cooldown after a mint for onesel args: [GBLIN_ZAP_ADDRESS as Hex, shares], }), }); - await wallet.waitForTransactionReceipt(approveHash); + const approval = await wallet.waitForTransactionReceipt(approveHash); + if (approval?.status === "reverted") { + return `Error redeeming GBLIN: approval ${approveHash} reverted.`; + } } const data = encodeFunctionData({ @@ -213,8 +219,11 @@ Note: the vault enforces a 20-second redemption cooldown after a mint for onesel data, }); const receipt = await wallet.waitForTransactionReceipt(txHash); + if (receipt?.status === "reverted") { + return `Error redeeming GBLIN: transaction ${txHash} reverted.`; + } - return `Redeemed ${args.gblinAmount} GBLIN for at least ${formatEther(minEthOut)} ETH. Tx: ${txHash}\nReceipt: ${JSON.stringify(receipt)}`; + return `Redeemed ${args.gblinAmount} GBLIN for at least ${formatEther(minEthOut)} ETH. Transaction hash: ${txHash}`; } catch (error) { return `Error redeeming GBLIN: ${error}`; } From af0c8b825810e074d79405c44f1b2136e2e84ee9 Mon Sep 17 00:00:00 2001 From: GBLIN Protocol Date: Tue, 22 Sep 2026 13:58:57 +0200 Subject: [PATCH 14/14] gblin: test that a reverted transaction is reported as an error --- .../gblin/gblinActionProvider.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts index 724d79f14..413148a13 100644 --- a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts @@ -10,7 +10,7 @@ import { } from "./constants"; const MOCK_TX_HASH = "0xabcdef1234567890"; -const MOCK_RECEIPT = { status: 1, blockNumber: 1234567 }; +const MOCK_RECEIPT = { status: "success", blockNumber: 1234567n }; const MOCK_ADDRESS = "0x9876543210987654321098765432109876543210"; const BPS = 10_000n; const DEFAULT_SLIPPAGE_BPS = 100n; @@ -61,6 +61,15 @@ describe("GBLIN Action Provider", () => { expect(response).toContain(MOCK_TX_HASH); }); + it("should report a reverted purchase as an error", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce([10n ** 16n, 0n, 0n]); + mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({ status: "reverted" }); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("reverted"); + }); + it("should reject a non-positive amount", async () => { const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0" }); expect(response).toContain("must be greater than 0");