Skip to content

test(aave): discover live role candidates from chain state - #203

Merged
nishuzumi merged 2 commits into
nishuzumi:mainfrom
zkasuran:test/aave-role-discovery
Sep 22, 2026
Merged

nishuzumi merged 2 commits into
nishuzumi:mainfrom
zkasuran:test/aave-role-discovery

Conversation

@zkasuran

Copy link
Copy Markdown
Contributor

What and why

Closes #201.

The four Aave mainnet role searches went red because the accounts seeded in LIVE_CANDIDATES moved their positions. Nothing in the adapter changed: the reserve list, metadata, bytecode and keyed ABI suites all pass on the 13 reserve set, the withdraw role still resolves, the same four fail identically on the previous head. Re-seeding the list by hand fixes it until the next time somebody empties a wallet, so the search gets a second pass instead.

Seeds and the reserve's own treasury still go first and still cost no request, so a market where they qualify reads no logs at all. When none of them qualifies, the search pages recent chain state for accounts that are active now:

  • roles that need an ERC-20 balance (supply, withdraw) read the token's own Transfer log, the underlying for supply and the aToken for withdraw;
  • roles that need a market position (account health, borrow, repay) read the Pool's own Supply log, because a supplier holds collateral and no ERC-20 balance can show that.

Both events come from the ABIs this package already derives, through getAbiItem, so nothing here hand-writes one. The zero address and the market's own position tokens are filtered out, since a burn is not a holder and a balance the protocol manages is not a balance an account controls.

rpc.monad.xyz caps eth_getLogs at a 100 block range (-32614 eth_getLogs is limited to a 100 range), so the scan pages backwards a window at a time under a block budget rather than asking for a wide range that would be refused. One scan per source is shared across the file, so a drifted fixture costs one scan rather than one per test.

I left the state-override route alone. It would have covered supply, borrow and repay, but accountData is a live Query rather than a simulation, so the health role could not be synthesised the same way and the file would have carried two mechanisms. Discovery covers all four. Happy to switch if you would rather the three simulated roles used stateOverrides and syntheticState.

Type of change

  • Protocol / Capability / Query
  • Core / simulator / MCP server
  • Bug fix
  • Documentation / example
  • Tooling / dependency

Framework and package impact

None. Test-only, one file. No public types, package boundaries, Capability tree or Change/Receipt behavior change, so there is no changeset.

Verification

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm test
  • User-facing package changes include a changeset (N/A, test-only)
  • Docs and examples match the implemented API

pnpm test is green on every package except packages/protocols/kuru, whose live native-swap test fails on the drifted Router pins in #194. That one reproduces on a pristine 4e3b985 with this diff stashed, so it is not from this change. The full offline suite passes on every package including kuru.

Protocol changes

  • Parameters separate reusable Zod value types from field-purpose descriptions
  • Every Capability owns one direct TransactionNode and one typed Receipt
  • Receipt tests preserve every original Change object in exact length and order
  • Positive and @ts-expect-error fixtures cover exported type behavior
  • Fixed addresses and ABIs include sources and verification
  • A live Monad happy path returns zero Warnings

The four Receipt assertions are untouched. Only where the account comes from changed.

Evidence

Before, on 4e3b985:

Tests  4 failed | 38 passed (42)
Error: no live account can play the Aave account health role: tried 3 candidates against 1 of the market's 13 reserves: USDC: 0x22A14267f9F8bA21D11898F0772e652B09e6A8A1 holds no collateral in the market; ...

After:

✓ Aave mainnet > reads account health and reserve rates  24699ms
✓ Aave mainnet > simulates a supply into an exhaustive typed Receipt  3542ms
✓ Aave mainnet > simulates a withdraw into an exhaustive typed Receipt  1866ms
✓ Aave mainnet > simulates a borrow as a declared inflow with no outflow  647ms
✓ Aave mainnet > simulates a repay of the debt the same run drew  997ms
Tests  43 passed (43)

Green twice against Monad mainnet, about an hour apart, at 23:36Z and 00:35Z.

Two controls, because a live suite that passes today proves less than one that fails for the right reason:

  1. Seeds pointed at two empty addresses, which is the acceptance criterion about the seeds emptying their positions. 43 passed. Nothing in the suite depends on the seeded list any more.
  2. Discovery budget set to zero, seeds restored. Exactly the original four fail again with the original reasons. The new offline test fails with them. So discovery is what carries these roles, not luck about who held what today.

Measurements behind the constants, taken 2026-09-16 at head 105384582:

source logs windows blocks time distinct accounts
USDC Transfer 261 1 100 0.3s 37
USDT0 Transfer 29 5 500 1.2s 15
aUSDC Transfer 9 40 4000 12.2s 4
Pool Supply 11 60 6000 17.4s 5

4 of the 5 discovered suppliers hold collateral and clear the borrowing power the role asks for. 4 of 6 discovered USDC recipients hold at least ROLE_UNITS. The 100 block cap was measured directly: 500, 2000 and 10000 block windows are all refused in 0.2s, one 100 block window answers in 0.38 to 0.42s.

The failure message now separates the passes, so an empty market reads differently from one the search never looked at:

no live account can play the Aave withdraw role: tried 3 candidates (3 seeded, 0 discovered from recent chain state) against 2 of the market's 13 reserves: ...

New offline coverage for the second pass: it finds a discovered account when no seed qualifies, reads the Pool's Supply field rather than a Transfer field, then discovers nobody from a log that only names protocol addresses.


AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by the author. Verified locally before submitting: pnpm lint (290 files), pnpm build, pnpm typecheck, the full offline suite, every package's live suite except the pre-existing kuru failure noted above, including the four Aave mainnet simulations at zero Warnings, run twice an hour apart plus the two falsification controls.

…#201)

The four Aave mainnet role searches failed because the seeded accounts
moved their positions. Re-seeding by hand fixes it until the next time,
so the search gets a second pass instead.

The seeds and the reserve's own treasury still go first and still cost no
request, so a market where they qualify reads no logs. When none of them
qualifies the search pages recent chain state for accounts that are active
now: a token's own Transfer log for the roles that need a balance, the
Pool's Supply log for the roles that need collateral. Both come from the
ABIs this package already derives rather than a hand-written event.

rpc.monad.xyz caps eth_getLogs at a 100 block range, so the scan pages
backwards a window at a time under a block budget. One scan per source is
shared across the file. A run on 2026-09-16 spent 24.7s on the first
role to want the Pool log and under a second on each of the two that
reused it.

The failure message now separates the two passes, so an empty market reads
differently from one the search never looked at. Receipt assertions,
Warnings and the exhaustive shape are unchanged.
@zkasuran

Copy link
Copy Markdown
Contributor Author

CI note, since ci is red here and the first-fail bail makes that hard to read (#195).

This change is green on CI's own runner. Inside the failing ci job, packages/protocols/aave reports Tests 43 passed (43), live against Monad mainnet from GitHub's network rather than mine. windows-offline passed too, at 35 passed / 8 skipped for aave.

The same job on main at 4e3b985 fails with exactly the four errors this PR is about:

packages/protocols/aave test:  FAIL  test/aave.test.ts > Aave mainnet > reads account health and reserve rates
packages/protocols/aave test: Error: no live account can play the Aave account health role: tried 3 candidates against 1 ...
packages/protocols/aave test:  FAIL  ... simulates a supply into an exhaustive typed Receipt
packages/protocols/aave test:  FAIL  ... simulates a borrow as a declared inflow with no outflow
packages/protocols/aave test:  FAIL  ... simulates a repay of the debt the same run drew

So the bail has moved past aave. What it lands on now is packages/protocols/merkl:

FAIL test-online/live-mainnet.test.ts > Merkl live Monad mainnet self-claim > builds and simulates a positive claim with exhaustive ordered evidence
Error: Merkl rewards request failed: fetch failed
  ❯ fetchMerklRewardCandidates src/api.ts:32:13

That is the runner failing to reach Merkl's off-chain rewards API, not a chain read. It is not from this diff, which touches one aave test file. ci is red the same way on main and on #202, so the job was never going to be green from here.

Locally every other package is green. The one exception is packages/protocols/kuru, whose live native-swap test trips the drifted Router pins in #194. That reproduces on a pristine 4e3b985 with this diff stashed.

@nishuzumi nishuzumi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audited exact head f9a9af04 against main@7943745, plus one small follow-up commit.

This is the right shape for #201: seeds and the treasury stay a request-free fast path, discovery only runs when they fail, both log sources come from the shipped ABIs through getAbiItem, and the failure message now says how many candidates each pass contributed. Live against Monad mainnet today: 43/43 in 56 s. Both falsification controls reproduce — seeds pointed at empty addresses still 43/43; DISCOVERY_WINDOWS = 0 fails exactly account health, borrow, repay and the new offline test — so discovery is what carries the roles. Three independent readers confirmed the three offline mutation cases (budget zero, Supply.user instead of onBehalfOf, protocol-address filter removed) each fail their intended case; the Receipt assertions in the four live tests are byte-identical to main; PROTOCOL_ADDRESSES and the discovered accounts are both checksummed so the filter cannot miss; the SCANS key is the shared rpcUrl, so health pays for the Supply scan and borrow/repay reuse it.

Pushed before the squash: scanned() now evicts a rejected promise (pending.catch(() => SCANS.delete(key))), so one transient eth_getLogs failure during account health no longer turns borrow and repay red with the same cached error; and >= at the window boundary so a head of exactly 100 is a 100-block window rather than 101 (unreachable live; exact for the stub).

Noted, not blocking: the theoretical worst case for supply/withdraw is one full 80-window scan per reserve across all 13 reserves if every discovered holder fails the balance check — implausible for stablecoin Transfer logs, and the memo caps it at one scan per token per run. If it ever bites, cap pass 2 to the first few reserves.

@nishuzumi
nishuzumi merged commit af9175a into nishuzumi:main Sep 22, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aave: seeded live role accounts have drifted; supply/borrow/repay/health mainnet tests cannot find a candidate

2 participants