test(aave): discover live role candidates from chain state - #203
Conversation
…#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.
|
CI note, since This change is green on CI's own runner. Inside the failing The same job on So the bail has moved past aave. What it lands on now is 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. Locally every other package is green. The one exception is |
…indow at the boundary
nishuzumi
left a comment
There was a problem hiding this comment.
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.
What and why
Closes #201.
The four Aave mainnet role searches went red because the accounts seeded in
LIVE_CANDIDATESmoved 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:
Transferlog, the underlying for supply and the aToken for withdraw;Supplylog, 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.xyzcapseth_getLogsat 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
accountDatais 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 usedstateOverridesandsyntheticState.Type of change
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 buildpnpm typecheckpnpm lintpnpm testpnpm testis green on every package exceptpackages/protocols/kuru, whose live native-swap test fails on the drifted Router pins in #194. That one reproduces on a pristine4e3b985with this diff stashed, so it is not from this change. The full offline suite passes on every package including kuru.Protocol changes
@ts-expect-errorfixtures cover exported type behaviorThe four Receipt assertions are untouched. Only where the account comes from changed.
Evidence
Before, on
4e3b985:After:
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:
Measurements behind the constants, taken 2026-09-16 at head
105384582:TransferTransferTransferSupply4 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:
New offline coverage for the second pass: it finds a discovered account when no seed qualifies, reads the Pool's
Supplyfield rather than aTransferfield, 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.