Skip to content

feat: add Aave V4 protocol (and update V3)#846

Merged
suisuss merged 11 commits intostagingfrom
feat/aave-v4-integration
Apr 21, 2026
Merged

feat: add Aave V4 protocol (and update V3)#846
suisuss merged 11 commits intostagingfrom
feat/aave-v4-integration

Conversation

@suisuss
Copy link
Copy Markdown

@suisuss suisuss commented Apr 14, 2026

Summary

Adds an ABI-driven protocol definition for Aave V4 to match what our existing Aave V3 integration enables. First Spoke only: Lido Spoke on Ethereum mainnet. The other launch Spokes (EtherFi, Kelp, Ethena Correlated, Ethena Ecosystem, Lombard BTC) share the same ABI and can be added in follow-ups.

Aave V4 launched on Ethereum mainnet on 2026-03-30 with a Hub-and-Spoke architecture. Users interact with Spokes (not Hubs) for supply/borrow. Hubs provide credit lines to Spokes and hold admin-only surfaces.

Files (V4 additions)

File Purpose
protocols/abis/aave-v4.json Reduced ABI covering 9 ISpoke functions
protocols/aave-v4.ts defineAbiProtocol() with lidoSpoke contract
tests/unit/protocol-aave-v4.test.ts 20 unit tests
tests/integration/protocol-aave-v4-onchain.test.ts 6 on-chain integration tests (mainnet, gated on INTEGRATION_TEST_MAINNET_RPC_URL)
protocols/index.ts Auto-generated barrel update
lib/types/integration.ts Auto-generated: drops "aave", adds "aave-v3" and "aave-v4" in IntegrationType

Action mapping (V3 -> V4)

V3 slug V4 slug Difference
supply supply First input asset: address -> reserveId: uint256. No referralCode.
withdraw withdraw Same shape change.
borrow borrow No interestRateMode (V4 has only one rate model, uses drawn/premium shares).
repay repay Same shape change.
set-collateral set-collateral Gains onBehalfOf param.
get-user-account-data get-user-account-data V4 returns a UserAccountData struct (tuple). readContractCore unwraps single unnamed tuples, and the template engine supports dotted paths (result.healthFactor). UI picker shows one "accountData" entry; subfields documented in description.
get-user-reserve-data Split into get-user-supplied-assets + get-user-debt V4 doesn't have a unified per-reserve read.
(n/a) get-reserve-id New utility. V4 uses opaque uint256 reserveId per Spoke; this resolves from (hub, assetId).

Write actions (supply, withdraw, borrow, repay) intentionally do not declare named outputs. The underlying writeContractCore returns result: undefined for all writes, so declared outputs would surface in the UI template autocomplete but resolve to undefined at runtime. Decoding write-action return values is tracked as a platform follow-up: KEEP-296. V3 follows the same convention (outputs on reads only).

Cross-cutting improvements

Two UX improvements motivated by V4 that affect all protocols:

  1. Chain selector now restricted to deployed chains. Added allowedChainIds to ActionConfigFieldBase. buildConfigFieldsFromAction() computes this from each contract's addresses map; ChainSelectField filters the dropdown. Previously the network dropdown showed every EVM chain regardless of where the protocol was deployed - users could pick Polygon for an action that only exists on Ethereum and the workflow would fail at runtime. Fix applies to every protocol, not just Aave.

  2. Aave V4 field tooltips link to official docs. The info icon cursor is pointer when docUrl is set and clicking the tooltip or icon opens the relevant Aave V4 docs page in a new tab. Infrastructure already existed in ProtocolFieldLabel (lib/extensions.tsx); the V4 input overrides populate docUrl where applicable.

V3 slug rename (folded in-PR)

Per @eskp review on lib/types/integration.ts. Shipping the rename alongside V4 so both versions land together with a clean slug story.

  • protocols/aave-v3.ts: slug: "aave" -> slug: "aave-v3"
  • Auto-regenerated: lib/types/integration.ts, protocols/index.ts (via pnpm discover-plugins)
  • lib/mcp/tools.ts: updated example strings in tool descriptions (3 occurrences)
  • scripts/seed/workflows/aave/ renamed to aave-v3/, JSON contents updated
  • scripts/pr-test/seed-pr-data.sql: inline workflow actionType and _protocolMeta.protocolSlug updated
  • scripts/README.md, docs/api/workflows.md: example strings updated
  • .claude/commands/test-protocol.md: slug-mapping example updated
  • Migration drizzle/0051_rename_aave_slug_to_v3.sql: data-only (no schema change). Rewrites workflows.featured_protocol, and the actionType + stringified _protocolMeta.protocolSlug inside workflows.nodes (jsonb). Text-level REPLACE on JSONB with key-scoped patterns so free-form text containing "aave" is untouched. Uses the same escape convention as the precedent 0048_rename_weth_to_wrapped.sql (doubled backslashes in the LIKE predicate to match the escaped inner-string form produced by JSON.stringify). Verified locally end-to-end: correct transforms, no false positives, idempotent.

Known caveats

  1. reserveId UX is opaque. Resolved. Chaining get-reserve-id output into supply/borrow via {{@node:Get Reserve ID.reserveId}} is the standard template-ref pattern. Verified in workflow-executor.ts (resolveConfigFieldPath) and template.test.ts. No new field type needed for v1.
  2. No integration tests. Resolved. 6 on-chain integration tests added targeting mainnet via INTEGRATION_TEST_MAINNET_RPC_URL (separate env var from the Sepolia-targeting INTEGRATION_TEST_RPC_URL). Tests confirm the Lido Spoke address is valid, the ABI matches deployed bytecode, and all functions respond correctly.
  3. getUserAccountData deferred. Resolved. readContractCore.ts unwraps single unnamed tuple returns into the raw decoded object. Template engine supports dotted paths (result.healthFactor). Integration tests pass against mainnet including the struct decode.
  4. Mainnet-only at launch. No L2 / testnet addresses yet.
  5. Only 1 of 6 Spokes exposed. Follow-up work to add EtherFi, Kelp, Ethena (x2), Lombard BTC.
  6. Write-action return decoding not implemented. Tracked as KEEP-296. Requires a platform-level decision on simulation strategy (pre-tx eth_call vs post-tx re-simulation vs event-based).

Test plan

  • pnpm test tests/unit/protocol-aave-v4.test.ts - 20/20
  • pnpm test tests/unit/protocol- - no regressions across existing protocols, including updated aave-v3 assertions
  • pnpm check protocols/aave-v4.ts tests/unit/protocol-aave-v4.test.ts - clean
  • pnpm type-check - clean
  • pnpm discover-plugins - registers cleanly, "aave-v4" added to IntegrationType
  • On-chain integration tests: 6/6 pass against mainnet Lido Spoke (including getUserAccountData struct decode)
  • V3 slug rename aave -> aave-v3 folded into this PR
  • Drizzle migration 0051 authored + locally verified (data-only, idempotent, key-scoped JSONB text replace; LIKE predicate mirrors the doubled-backslash convention from the precedent 0048_rename_weth_to_wrapped.sql)
  • Chain selector restriction verified on Aave V3 (5 chains) and Aave V4 (1 chain)

Open items (not blockers)

  • Manual UI QA in workflow builder: verify type-aware fields render for reserveId uint and usingAsCollateral bool; verify chain dropdown shows only Ethereum Mainnet for Aave V4 actions; verify tooltip cursor and doc-link behaviour
  • Staging deploy verification: confirm migration 0051 runs cleanly and existing staging workflows using aave/* actionTypes continue to execute post-migration
  • Follow-up PR: add the 5 other Lido Spoke siblings (EtherFi, Kelp, Ethena x2, Lombard BTC)
  • Follow-up platform ticket: KEEP-296 (write-action return value decoding)

@eskp eskp requested review from a team, OleksandrUA, eskp and joelorzet and removed request for a team April 15, 2026 00:07
Comment thread lib/types/integration.ts Outdated
suisuss added a commit that referenced this pull request Apr 15, 2026
Per review on PR #846 - the V3 protocol's short slug "aave" conflicts
with the expectation that both V3 and V4 coexist clearly. Rename to
"aave-v3" so version is explicit in every reference (IntegrationType,
actionType, MCP tool invocations, featured_protocol metadata).

Code changes:
- protocols/aave-v3.ts: slug "aave" -> "aave-v3"
- tests/unit/protocol-aave-v3.test.ts: assertions updated
- lib/types/integration.ts, protocols/index.ts: auto-regenerated
- lib/mcp/tools.ts: update example strings in tool descriptions
- scripts/seed/workflows/aave/ renamed to aave-v3/, contents updated
- scripts/pr-test/seed-pr-data.sql: actionType and protocolSlug updated
- scripts/README.md, docs/api/workflows.md: example strings
- .claude/commands/test-protocol.md: slug-mapping example

Migration 0048_rename_aave_slug_to_v3.sql:
Data-only migration (no schema change) that rewrites existing workflows
in place:
- workflows.featured_protocol: "aave" -> "aave-v3"
- workflows.nodes (jsonb): each node's data.config.actionType from
  "aave/*" to "aave-v3/*", and the stringified
  data.config._protocolMeta.protocolSlug from "aave" to "aave-v3"

Uses text-level REPLACE on the canonical JSONB text rendition. Two
distinct patterns because PostgreSQL's JSONB canonicalization adds
spaces after colons at the top level ("actionType": "aave/...") but
JSON.stringify output inside _protocolMeta has no spaces
(\"protocolSlug\":\"aave\"). Both patterns are key-scoped so free-form
text containing the word "aave" (e.g. descriptions) is untouched.

Verified locally against a seeded workflow: all three aave references
correctly rewritten, web3/write-contract action and a description
string containing the word "aave" were left alone. Re-running the
migration is a no-op (idempotent).
suisuss added a commit that referenced this pull request Apr 16, 2026
Per review on PR #846 - the V3 protocol's short slug "aave" conflicts
with the expectation that both V3 and V4 coexist clearly. Rename to
"aave-v3" so version is explicit in every reference (IntegrationType,
actionType, MCP tool invocations, featured_protocol metadata).

Code changes:
- protocols/aave-v3.ts: slug "aave" -> "aave-v3"
- tests/unit/protocol-aave-v3.test.ts: assertions updated
- lib/types/integration.ts, protocols/index.ts: auto-regenerated
- lib/mcp/tools.ts: update example strings in tool descriptions
- scripts/seed/workflows/aave/ renamed to aave-v3/, contents updated
- scripts/pr-test/seed-pr-data.sql: actionType and protocolSlug updated
- scripts/README.md, docs/api/workflows.md: example strings
- .claude/commands/test-protocol.md: slug-mapping example

Migration 0048_rename_aave_slug_to_v3.sql:
Data-only migration (no schema change) that rewrites existing workflows
in place:
- workflows.featured_protocol: "aave" -> "aave-v3"
- workflows.nodes (jsonb): each node's data.config.actionType from
  "aave/*" to "aave-v3/*", and the stringified
  data.config._protocolMeta.protocolSlug from "aave" to "aave-v3"

Uses text-level REPLACE on the canonical JSONB text rendition. Two
distinct patterns because PostgreSQL's JSONB canonicalization adds
spaces after colons at the top level ("actionType": "aave/...") but
JSON.stringify output inside _protocolMeta has no spaces
(\"protocolSlug\":\"aave\"). Both patterns are key-scoped so free-form
text containing the word "aave" (e.g. descriptions) is untouched.

Verified locally against a seeded workflow: all three aave references
correctly rewritten, web3/write-contract action and a description
string containing the word "aave" were left alone. Re-running the
migration is a no-op (idempotent).
@suisuss suisuss force-pushed the feat/aave-v4-integration branch from 341817b to 2e9307f Compare April 16, 2026 02:10
@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

suisuss added a commit that referenced this pull request Apr 16, 2026
Adds the getUserAccountData read action which returns a UserAccountData
struct (tuple) with 7 fields: riskPremium, avgCollateralFactor,
healthFactor, totalCollateralValue, totalDebtValueRay,
activeCollateralCount, borrowCount.

Verified at the code level that this works end-to-end:
- readContractCore.ts:238-252 unwraps single unnamed tuple returns to
  the raw decoded object, making struct fields directly accessible
- The template engine (workflow-executor.ts:454-482) supports dotted
  paths: {{@node:Label.result.healthFactor}} resolves correctly
- Unit tests in template.test.ts confirm nested path access

The UI output picker shows one "accountData" entry (type tuple) rather
than 7 individual fields - users discover the subfields via the action
description or MCP schema. Acceptable for v1.

Also resolves the reserveId UX question: chaining get-reserve-id output
into supply/borrow via template refs is the standard supported pattern.
No new field type needed.

Closes both remaining draft caveats from PR #846.
@suisuss suisuss marked this pull request as ready for review April 16, 2026 03:03
@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

The previous commit (6c6fe54) only dropped the custom output overrides
from write actions, but ABI-derived outputs still surfaced as UI template
suggestions (result0/result1) that resolve to undefined at runtime, since
deriveActionsFromAbi unconditionally attaches outputs from the ABI.

Fix at the UI consumption layer: gate action.outputs -> outputFields in
buildOutputFieldsFromAction on action.type === "read". The protocol model
remains unchanged (protocol-abi-derive.test.ts already asserts that writes
do get ABI outputs at the model layer -- that is correct, it is purely
the UI surfacing that was lying).

Applies to every ABI-driven protocol's write actions, not just Aave V4.
No user-visible regression: the template references that disappear from
autocomplete only ever resolved to undefined.

Adds two regression tests exercising protocolActionToPluginAction to
assert the behaviour both ways (writes hide ABI outputs, reads surface
them).

Follow-up: KEEP-296 (decode write-action return values for protocol actions)
…ions.type

Schema audit surfaced two slug-bearing surfaces the original 0051 missed:

  * workflows.nodes[].data._eventProtocolSlug -- event trigger nodes store
    the protocol slug here (separate from action nodes' actionType /
    _protocolMeta). Modelled after the 0025 safe-wallet precedent using
    jsonb_set via jsonb_agg, since _eventProtocolSlug is a native jsonb
    key (unlike the stringified _protocolMeta).

  * integrations.type -- $type<IntegrationType>. Protocol plugins set
    requiresCredentials: false so no rows are expected in practice, but
    adding the rename is idempotent and cheap insurance.

Not touched:
  * _eventProtocolIconPath -- icon file itself is unchanged across the
    rename (protocols/aave-v3.ts still declares "/protocols/aave.png"),
    unlike the 0025 safe-wallet case which had to rewrite the icon path.
  * Historical execution tables (workflow_executions, workflow_execution_logs,
    direct_executions) -- rewriting them would falsify past-run history.

Migration header now lists the audited surfaces explicitly.
@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

Previous pattern swallowed any error that didn't contain three specific
strings into a passing test:

  try {
    // positive assertions -- only run if RPC succeeds
  } catch (error) {
    expect(String(error)).not.toContain("INVALID_ARGUMENT");
    expect(String(error)).not.toContain("could not decode");
    expect(String(error)).not.toContain("invalid function");
  }

An RPC timeout, a rate-limit, an unrelated revert reason, or a genuine
ABI mismatch whose error string happened to miss those three needles would
all pass green with no positive assertion actually running.

Rewrite:
  - Read tests: remove try/catch. If provider.call fails or the return
    can't be decoded, the test fails loudly (as it should).
  - Write tests (supply, setUsingAsCollateral): switch from estimateGas
    to provider.call with the zero-balance TEST_ADDRESS to trigger a
    business-logic revert, and assert rejects.toMatchObject({ code:
    "CALL_EXCEPTION" }) -- ethers v6's canonical code for contract-level
    reverts. An ABI-level failure (INVALID_ARGUMENT, BAD_DATA) or an
    unknown selector raises a different error class, so the matcher
    distinguishes "calldata was understood by the contract" from
    "calldata never got that far".

Test count unchanged (6). Integration tests remain gated on
INTEGRATION_TEST_MAINNET_RPC_URL, so this change doesn't affect the
default CI path.
@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

Running the suite against mainnet revealed that setUsingAsCollateral
silently succeeds on reserveId=0 (the Spoke no-ops on nonexistent
reserves rather than reverting), returning "0x". My previous assertion
rejects.toMatchObject({ code: "CALL_EXCEPTION" }) was too narrow --
it assumed every write would revert for a zero-balance caller.

What the write tests are actually proving is "the deployed bytecode
understood our calldata". Both a clean return ("0x" for void functions)
and CALL_EXCEPTION are valid evidence of that. What we still reject:
INVALID_ARGUMENT, BAD_DATA, BUFFER_OVERRUN -- ABI-level errors that
would signal the protocol definition doesn't match the deployed contract.

Extracted the try/catch into expectCallAcceptedByBytecode() so supply
and setUsingAsCollateral share the assertion shape.

Verified: all 6 integration tests pass against eth-mainnet.
@suisuss suisuss merged commit 84212d5 into staging Apr 21, 2026
29 checks passed
@suisuss suisuss deleted the feat/aave-v4-integration branch April 21, 2026 06:26
@github-actions
Copy link
Copy Markdown

🧹 PR Environment Cleaned Up

The PR environment has been successfully deleted.

Deleted Resources:

  • Namespace: pr-846
  • All Helm releases (Keeperhub, Scheduler, Event services)
  • PostgreSQL Database (including data)
  • LocalStack, Redis
  • All associated secrets and configs

All resources have been cleaned up and will no longer incur costs.

@github-actions
Copy link
Copy Markdown

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@eskp eskp mentioned this pull request Apr 21, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants