Skip to content

Add bridge-then-deploy action provider#3

Open
zaryab2000 wants to merge 2 commits into
feat/bridgeThenDeployfrom
claude/tender-mccarthy-ROhys
Open

Add bridge-then-deploy action provider#3
zaryab2000 wants to merge 2 commits into
feat/bridgeThenDeployfrom
claude/tender-mccarthy-ROhys

Conversation

@zaryab2000

Copy link
Copy Markdown
Owner

Summary

Adds a new bridge-then-deploy ActionProvider (bridgeDeploy) that composes an Across bridge with a destination lending/vault supply, letting an agent express "move this capital to chain X and put it to work" as a single intent.

Central design constraint (honest by design)

Across destination-side execution only delivers to a deployed handler contract that implements handleV3AcrossMessage(...)never to a plain EOA. Agent wallets are EOAs, so this flow is deliberately two-step and non-atomic: bridge → poll status → supply on arrival. The provider never claims the supply has happened before the bridge fills.

Actions

  • bridge_and_deploy — initiates the Across deposit and records the pending destination supply; returns a depositId and status: "bridging". Does not supply yet.
  • bridge_deploy_status — polls the Across deposit-status API; once filled, auto-runs the recorded destination supply and returns the deploy tx hash. Reports pending/refunded otherwise.
  • deploy_on_destination — supplies an already-bridged token into Compound (Comet supply) or Morpho (vault deposit) on Base. Performs a balance preflight, so it is safe to retry to recover from a partial failure.

Scope

  • Destination deploy gated to Base (8453/84532); origin can be any EVM chain Across supports.
  • v1 ships Flow A (EOA, two-step) only. Flow B (handler-contract atomic deposit) is documented in the README as the upgrade path.

Conventions

  • All actions return Promise<string>; errors are returned, not thrown.
  • Zod v4 schemas with .describe() on every field; no .strip().
  • Reuses the shared approve util and mirrors the in-tree across provider's SDK wiring (privateKey constructor config, fully mocked in tests).
  • Factory re-exported from src/action-providers/index.ts.

Checks

  • pnpm test — full agentkit suite green (875 tests, incl. 11 new for this provider, mocked wallet + Across SDK).
  • pnpm run lint, pnpm run format, pnpm build — all clean.
  • Changeset added (@coinbase/agentkit, patch, past tense).

Note: per-provider README.md prominently documents the two-step / non-atomic / EOA-can't-receive-callback semantics and the partial-failure recovery path.

https://claude.ai/code/session_013rnmDYrKDMctusY33uneiy


Generated by Claude Code

Composes an Across bridge with a destination lending/vault supply so an
agent can express "move this capital to chain X and put it to work" as one
intent. Honest about the EOA constraint: Across destination execution only
reaches a handler contract, never an EOA, so the flow is two-step and
non-atomic (bridge -> poll status -> supply on arrival).

Actions:
- bridge_and_deploy: initiates the Across deposit and records the pending
  destination supply; never claims the supply has happened.
- bridge_deploy_status: polls the Across deposit-status API and auto-runs
  the recorded supply once the bridge is filled.
- deploy_on_destination: supplies an already-bridged token into Compound or
  Morpho on Base, with a balance preflight so it is safe to retry.

Destination deploy is gated to Base; origin can be any EVM chain Across
supports. Includes unit tests (mocked wallet + Across SDK), README
documenting the non-atomic semantics, and a changeset.

https://claude.ai/code/session_013rnmDYrKDMctusY33uneiy
@github-actions github-actions Bot added documentation Improvements or additions to documentation typescript action provider labels Jun 6, 2026
@zaryab2000

Copy link
Copy Markdown
Owner Author

@claude start review

@claude

claude Bot commented Jun 6, 2026

Copy link
Copy Markdown

Claude finished @zaryab2000's task in 2m 59s —— View job


Code Review — bridgeDeploy ActionProvider

Tasks

  • Load tools
  • Read all changed files (bridgeDeployActionProvider.ts, utils.ts, schemas.ts, constants.ts, index.ts, test, README, changeset)
  • Analyze against AgentKit conventions (CLAUDE.md)
  • Review for bugs, security, correctness
  • Post final review

Overall the code is well-structured and closely follows AgentKit conventions. All three major checks from CLAUDE.md pass (Promise, Zod v4 with .describe(), arrow-function supportsNetwork). Below are the findings in priority order.


🔴 High — Correctness Bugs

1. In-memory #pendingDeploys is lost on provider re-instantiation

bridgeDeployActionProvider.ts:46

#pendingDeploys: Map<string, PendingDeploy> = new Map();

#pendingDeploys is instance-level state. The factory bridgeDeployActionProvider(config) creates a new instance each time it is called. In most agent patterns the provider is re-created between sessions (or even between turns). If the provider is re-instantiated while a bridge is in flight — which can take minutes to an hour — the pending deploy record is gone. bridge_deploy_status will then return the "no pending supply was recorded" fallback instead of auto-supplying.

This either needs to be documented prominently as a hard constraint ("the same provider instance must be reused across calls") or the pending state must be serialized to somewhere durable (a file, a passed-in store, etc.). At minimum the README should warn that a new instance forgets all pending deploys.

Fix this →


2. Compound supply() silently ignores recipient

utils.ts:339-344

if (params.protocol === "compound") {
  data = encodeFunctionData({
    abi: COMET_SUPPLY_ABI,
    functionName: "supply",
    args: [token, atomicAmount],   // ← no recipient
  });
}

The supply(address asset, uint amount) Comet function deposits from msg.sender into msg.sender's own position. There is no recipient argument here. If the caller passes recipient ≠ walletProvider.getAddress(), for Morpho the deposit goes to the intended recipient, but for Compound it silently goes to the caller's position instead. This is a silent semantic divergence.

Comet provides supplyTo(address dst, address asset, uint amount) for this purpose. The ABI in constants.ts and the encodeFunctionData call should use supplyTo when recipient != walletProvider.getAddress(), or the COMET_SUPPLY_ABI should be updated to include supplyTo and always use it.

Fix this →


3. Quoted outputAmount stored as supply target — mismatches actual fill

bridgeDeployActionProvider.ts:112-117

this.#pendingDeploys.set(this.#pendingKey(...), {
  ...
  amount: deposit.outputAmount,   // ← from quote, not actual fill
  ...
});

deposit.outputAmount is the quoted output. The actual fill amount may differ. When deployToProtocol later runs the balance preflight (balance < atomicAmount), it uses the quoted amount, which can either:

  • Block the supply if the fill was slightly less than quoted (e.g., fee changed between quote and fill)
  • Over-supply if the fill was more than quoted (e.g., the relayer undercut the fee)

The safer approach is to supply Math.min(balance, atomicAmount) — i.e., use whatever landed — or to drop the preflight comparison and let the protocol revert if underfunded.


🟡 Medium — Reliability / Security

4. depositId interpolated into URL without encoding

utils.ts:255-258

const response = await fetch(
  `${ACROSS_DEPOSIT_STATUS_API}?originChainId=${originChainId}&depositId=${depositId}`,

depositId (and originChainId) are user-supplied strings concatenated directly into the URL. If either contains &, =, or #, the query string is silently malformed. Use URLSearchParams:

const url = new URL(ACROSS_DEPOSIT_STATUS_API);
url.searchParams.set("originChainId", String(originChainId));
url.searchParams.set("depositId", depositId);
const response = await fetch(url.toString(), { method: "GET" });

Fix this →


5. Two test paths not covered

bridgeDeployActionProvider.test.ts

  • filled with no pending deploybridgeDeployStatus returns a "call deploy_on_destination manually" note when the map has no entry for the deposit. bridgeDeployActionProvider.ts:178-187 handles this but it is never exercised.
  • Supply fails on filledbridgeDeployActionProvider.ts:191-198 keeps the pending deploy for retry when deployResult.startsWith("Error"). This retry path has no test.

6. Floating-point slippage arithmetic

utils.ts:183-185

const slippageBps =
  ((Number(formattedInput) - Number(formattedOutput)) / Number(formattedInput)) * 10000;

Converting bigintformatUnitsNumber → arithmetic introduces floating-point rounding. For basis-point comparisons, prefer bigint arithmetic directly:

const slippageBps = Number(
  ((quote.deposit.inputAmount - quote.deposit.outputAmount) * 10000n) / quote.deposit.inputAmount
);

(The existing across provider has the same issue — it is a pre-existing pattern, but worth fixing here.)


🟢 Low — Minor / Style

7. Dead validation in constructor (bridgeDeployActionProvider.ts:56-57)

const account = privateKeyToAccount(this.#privateKey as Hex);
if (!account) throw new Error("Invalid private key");

privateKeyToAccount from viem throws on an invalid key — it never returns null or undefined. The if (!account) branch is unreachable. (Same pattern in the upstream across provider.)


8. http() with no RPC URL (utils.ts:107)

transport: http(),

http() with no argument uses the chain's built-in public RPC, which is rate-limited. This is fine for occasional test runs but can fail under production load. (Same pattern in across provider — pre-existing.)


9. Token identifier inconsistency between actions

bridge_and_deploy accepts token as a symbol ("USDC"); deploy_on_destination accepts token as an address ("0x833..."). This is noted in the README, but the inconsistency creates a real risk that an LLM agent passes a symbol to the standalone deploy action and hits a silent failure (the Zod regex rejects non-address strings, so at least it fails loudly).

Consider adding a note to the deploy_on_destination action description explicitly warning: "token must be the destination-chain ERC-20 address, not the symbol".


Conventions Checklist

Rule Status
All actions return Promise<string>; errors returned not thrown
Zod v4 schemas with .describe() on every field; no .strip()
supportsNetwork is an arrow-function property
Factory bridgeDeployActionProvider exported and re-exported from index.ts
README.md present
pnpm run changeset — patch, past tense, correct package
Tests present and mocking wallet provider
super("bridgeDeploy", []) constructor call
Network.chainId compared as string

Summary: The implementation is clean and well-documented. The two blocking issues before upstream merge are the Compound supplyTo vs supply recipient bug (#2) and the in-memory state loss documentation (#1). The URL encoding (#4) and slippage arithmetic (coinbase#6) are simple one-liners to fix. The missing test coverage (#5) should be added before the PR goes out.

- Compound: use supplyTo(dst, asset, amount) so the position is credited to
  the recipient instead of msg.sender (silent divergence from Morpho).
- Auto-deploy: supply the amount that actually landed when a fill comes in
  under the quoted output, instead of failing the stale-quote preflight;
  deploy_on_destination stays strict for explicit calls.
- getDepositStatus: build the status URL with URLSearchParams.
- Slippage check now uses bigint arithmetic to avoid float rounding.
- Document the in-memory pending-deploy state (lost on re-instantiation) in
  the README and on the field; clarify token is an address (not symbol) for
  deploy_on_destination.
- Add tests for the filled-without-pending-record and supply-fails-retry paths.

https://claude.ai/code/session_013rnmDYrKDMctusY33uneiy

Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed in 8633b59. Summary of what changed and why:

Fixed

Intentionally deferred (with rationale)


Generated by Claude Code

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

Labels

action provider documentation Improvements or additions to documentation typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants