Vault Allocation History producer contract
Implement the Envio producer-side contract required by Kong's planned Vault Allocation History feature.
This specification supersedes the previous issue body. It deliberately separates deployment provenance from active allocator assignment, distinguishes accounting identity from historical completeness, and prevents archive-RPC failures from halting the shared Yearn Envio indexer.
Decisions
- Extend the existing Envio project. Use the root configuration, schema, generated code, and handler entrypoint. After cutover there is one active deployment and database. Do not create a second permanent Envio server or nested Envio package.
- Ethereum is the required first production candidate.
- Sonic (
chainId = 146) is explicitly out of scope and does not need to be indexed, audited, configured, or documented as a gap for this feature.
- Envio supplies replayable events, historical vault accounting, allocator provenance/assignments, and coverage evidence. Kong decides whether those inputs are sufficient to publish a complete timeline.
- The event scope is actual allocation state, allocator targets, actor inputs, and intentional idle policy. Explaining every allocator execution-eligibility setting is not part of this issue.
- Persist all machine-key addresses as lowercase
0x strings. Checksum formatting is a presentation concern.
- Preserve the existing indexer's typed entities and behavior. The allocation entities and handlers are additive. Deployment must use a candidate revision and a rollback plan because full historical replay may still be required.
Objective and ownership boundary
The shared Envio deployment must expose:
- Deterministically ordered, normalized allocation-related events.
- Top-level transaction envelope metadata.
- Immutable debt-allocator deployment bindings.
- Time-ordered RoleManager debt-allocator assignments.
- Exact block-end
totalAssets, totalDebt, and totalIdle checkpoints at accounting-changing blocks.
- Machine-readable evidence describing the historical range that has actually been validated.
Kong will consume these primitives to build:
- Complete per-strategy allocation states and transitions.
- Same-block grouping and
effects[].
- Actor and intent classification.
- Strategy/vault metadata enrichment.
- DOA matching and proposal status.
- Bps calculations.
- Redis-cached
VaultAllocationTimeline responses.
Envio must not implement Kong's finished timeline, DOA logic, transition classifications, public REST route, or Redis cache.
Do not add per-strategy archive reads. Kong can replay strategy state from events:
- Current debt:
DebtUpdated.new_debt and StrategyReported.current_debt.
- Strategy lifecycle:
StrategyChanged.
- Last report:
StrategyReported.blockTimestamp.
- Maximum debt:
UpdatedMaxDebtForStrategy.
- Allocator targets:
UpdateStrategyDebtRatios.
Single-deployment and failure-domain requirement
Implement allocation history inside the existing Envio project. It uses one:
- Root Envio configuration and generated-code pass.
- Root schema and database/Hasura deployment.
- Root handler entrypoint, with allocation helpers kept in normal source modules.
- Envio server after cutover.
- Health/freshness monitoring and deployment runbook.
The allocation feature is Ethereum-only for its first production candidate, even though the shared indexer supports several chains. Allocation handlers and archive calls must therefore have an explicit chain guard. Existing non-allocation entities and handlers must keep their current multichain behavior.
Archive-RPC reads still fail closed for checkpoint data: never write zeroes, partial values, or an unverified checkpoint. However, a final archive-RPC failure must be caught by the allocation handler. It must write a sanitized, queryable failure record, leave the checkpoint absent, keep safeForTimeline = false, and allow the shared indexer to continue. Never store credentials or RPC URLs in the failure record.
Envio's crossChain: false setting remains required because it isolates Effect cache and rate-limit budgets by chain. It does not provide process isolation, so the handler-level failure boundary is mandatory.
Adding persisted event configuration is not compatible with resuming the currently initialized database. The rollout must therefore use fresh candidate storage for the full replay, keep current production available during validation, and leave one active deployment/database after cutover.
Delivery gates
Implement and validate these in order. Do not claim the full issue complete when an earlier gate passes.
- Normalized event contract
- Allocator provenance and assignment history
- Historical accounting checkpoints
- Coverage certification, parity, and rollout contract
Each gate must retain focused unit/golden tests and evidence in the final report.
Gate 1: normalized allocation-event contract
Required event scope
V3 vault
Deposit
Withdraw
DebtUpdated
StrategyReported
StrategyChanged
UpdatedMaxDebtForStrategy
DebtPurchased
UpdateDefaultQueue
UpdateUseDefaultQueue
UpdateMinimumTotalIdle
UpdateAutoAllocate
Shutdown
RoleSet
RoleStatusChanged
UpdateRoleManager
UpdateAccountant
Deposit and Withdraw are required because idle-only flows change allocation percentages. UpdateMinimumTotalIdle and UpdateAutoAllocate describe intentional idle policy; Shutdown changes future allocation behavior.
RoleManager
AddedNewVault
RemovedVault
UpdateDebtAllocator
Debt allocator factory
Support each ABI variant actually deployed on an included chain. Do not assume an event name or topic0 proves that indexed/non-indexed parameter layout is identical.
Generic debt allocator
UpdateStrategyDebtRatios
UpdateKeeper
GovernanceTransferred
Operational feasibility settings such as minimum change/wait, maximum acceptable base fee, maximum accepted update loss, pause state, and manager configuration are non-goals for the first contract. Add them only in a separately approved expansion.
Entity contract
Add an entity equivalent to:
type AllocationSourceEvent
@index(fields: ["chainId", "vaultAddress", "blockNumber", "transactionIndex", "logIndex", "id"]) {
id: ID!
chainId: Int! @index
vaultAddress: String! @index
sourceAddress: String! @index
sourceType: String! @index
eventName: String! @index
signature: String!
normalizationVersion: Int!
abiVariant: String
blockNumber: Int! @index
blockTimestamp: BigInt! @index
blockHash: String!
transactionHash: String! @index
transactionIndex: Int!
logIndex: Int!
topLevelTransactionFrom: String @index
topLevelTransactionTo: String @index
topLevelInputSelector: String
strategyAddress: String @index
argsJson: String!
}
Allowed sourceType values:
vault
roleManager
debtAllocatorFactory
debtAllocator
Requirements:
- ID:
${chainId}:${transactionHashLower}:${logIndex}.
signature is the lowercase event topic0.
- Addresses, hashes, selectors, bytes, and topic0 use lowercase
0x encoding.
topLevelInputSelector is exactly the first four calldata bytes (0x plus eight hex characters), otherwise null.
- These transaction fields describe only the top-level envelope. They do not claim to identify internal Safe signers, nested callers, final internal targets, or nested function selectors. Traces and nested calldata decoding are not supplied by this issue.
- Fetch
to and input only for the allocation project's event set; do not expand the primary indexer's global transaction payload.
- Use
normalizationVersion = 1 initially.
abiVariant identifies the audited event layout/release/code-hash family when variants exist; it is null only when there is exactly one proven layout.
The normalized argsJson keys for normalization version 1 are fixed as follows. Key order is exactly the order shown:
| Source |
Event |
Ordered JSON keys |
| vault |
Deposit |
sender, owner, assets, shares |
| vault |
Withdraw |
sender, receiver, owner, assets, shares |
| vault |
DebtUpdated |
strategy, currentDebt, newDebt |
| vault |
StrategyReported |
strategy, gain, loss, currentDebt, protocolFees, totalFees, totalRefunds |
| vault |
StrategyChanged |
strategy, changeType |
| vault |
UpdatedMaxDebtForStrategy |
sender, strategy, newDebt |
| vault |
DebtPurchased |
strategy, amount |
| vault |
UpdateDefaultQueue |
newDefaultQueue |
| vault |
UpdateUseDefaultQueue |
useDefaultQueue |
| vault |
UpdateMinimumTotalIdle |
minimumTotalIdle |
| vault |
UpdateAutoAllocate |
autoAllocate |
| vault |
Shutdown |
no keys; serialize as {} |
| vault |
RoleSet |
account, role |
| vault |
RoleStatusChanged |
role, status |
| vault |
UpdateRoleManager |
roleManager |
| vault |
UpdateAccountant |
accountant |
| roleManager |
AddedNewVault |
vault, debtAllocator, category |
| roleManager |
RemovedVault |
vault |
| roleManager |
UpdateDebtAllocator |
vault, debtAllocator |
| debtAllocatorFactory |
NewDebtAllocator |
allocator, vault, followed by originalAllocator only for a proven ABI variant that emits it |
| debtAllocator |
UpdateStrategyDebtRatios |
strategy, newTargetRatio, newMaxRatio, newTotalDebtRatio |
| debtAllocator |
UpdateKeeper |
keeper, allowed |
| debtAllocator |
GovernanceTransferred |
previousGovernance, newGovernance |
All integer-valued fields in this table, including enum-like values such as changeType, role, status, and category, are decimal strings. Address arrays preserve emitted order. ABI variants with different indexed layouts normalize to the same keys; abiVariant identifies which decoder produced them. Do not add or rename keys without incrementing normalizationVersion.
Versioned serializer contract
Do not serialize event.params generically. Define an explicit serializer per supported event and ABI variant. For every serializer, document and golden-test:
- Exact field names and insertion order.
- Integers as base-10 strings.
- Lowercase addresses.
- Booleans as JSON booleans.
- Bytes as lowercase
0x strings.
- Enum representation.
- Array order.
- Absent fields versus explicit null.
- The event signature/topic0 and ABI layout used.
argsJson must be deterministic byte-for-byte for identical input. It must not contain arbitrary RPC responses.
Gate 1 acceptance
- Every required event/ABI variant has an explicit serializer and golden fixture.
- Ordering and IDs are deterministic.
- Top-level transaction metadata is correctly named and extracted.
- Empty/short calldata yields a null selector.
- Reprocessing identical input produces identical rows.
- Existing primary-indexer entities and queries remain unchanged.
Gate 2: allocator provenance and assignment history
NewDebtAllocator is deployment provenance, not complete active-assignment history. Model the facts separately.
Deployment binding
type DebtAllocatorDeployment {
id: ID! # `${chainId}:${allocatorLower}`
chainId: Int! @index
allocatorAddress: String! @index
vaultAddress: String! @index
factoryAddress: String! @index
originalAllocatorAddress: String
abiVariant: String
createdBlock: Int!
createdTimestamp: BigInt!
createdTransactionHash: String!
createdEventId: String!
}
Populate this immutable binding from NewDebtAllocator. If an ABI variant does not emit originalAllocatorAddress, leave it null and identify the variant; do not infer it.
RoleManager assignment history
type VaultDebtAllocatorAssignment
@index(fields: ["chainId", "vaultAddress", "blockNumber", "transactionIndex", "logIndex"]) {
id: ID! # normalized source event ID
chainId: Int! @index
vaultAddress: String! @index
allocatorAddress: String! @index
roleManagerAddress: String! @index
assignmentType: String! # `initial` or `updated`
implementationRecognition: String! # `knownGenericAllocator`, `other`, or `unknown`
blockNumber: Int! @index
blockTimestamp: BigInt!
blockHash: String!
transactionHash: String!
transactionIndex: Int!
logIndex: Int!
sourceEventId: String!
}
AddedNewVault creates the initial assignment.
UpdateDebtAllocator creates later assignments.
- An assigned address is a RoleManager fact; it is not automatically a recognized GenericDebtAllocator deployment.
RemovedVault closes RoleManager membership but must not delete historical assignments.
If a recognized factory deployment binding contradicts a RoleManager assignment, surface the disagreement in a queryable conflict entity or explicit assignment status. Never overwrite one fact with the other.
Dynamic-registration same-block requirement
Envio may retrospectively return logs earlier in the same block from a dynamically registered contract. Before using immediate allocator-to-vault lookup with hard failure:
- Audit every supported allocator factory/implementation ABI variant.
- Add a fixture proving no required allocator event can precede
NewDebtAllocator in the registration block.
If that invariant cannot be proven, implement deterministic block-level/deferred association. In either case:
- Never silently drop allocator events.
- Never use a zero-address vault.
- Never misclassify an arbitrary RoleManager assignee as a known GenericDebtAllocator.
- Persist/query an explicit unresolved or conflict state if association cannot be completed safely.
Gate 2 acceptance
- Initial and updated RoleManager assignments replay correctly.
- Deployment binding and active assignment remain independently queryable.
- Known allocator events resolve to the correct vault.
- Unknown role holders remain distinguishable from known allocator implementations.
- Conflicts and unresolved associations are visible.
- Same-block registration behavior is proven by fixtures or handled through deferred reconciliation.
Gate 3: historical vault accounting checkpoints
Checkpoint contract
type VaultAccountingCheckpoint
@index(fields: ["chainId", "vaultAddress", "blockNumber"]) {
id: ID! # `${chainId}:${vaultLower}:${blockNumber}`
chainId: Int! @index
vaultAddress: String! @index
blockNumber: Int! @index
blockTimestamp: BigInt! @index
blockHash: String!
totalAssets: BigInt!
totalDebt: BigInt!
totalIdle: BigInt!
accountingIdentityHolds: Boolean!
canonicalBlockVerified: Boolean!
source: String! # `archive-rpc-effect`
sourceEventIds: [String!]!
}
Also expose unresolved and resolved archive-read failures:
type VaultAccountingCheckpointFailure
@index(fields: ["chainId", "vaultAddress", "blockNumber", "resolved"]) {
id: ID! # `${chainId}:${vaultLower}:${blockNumber}`
chainId: Int! @index
vaultAddress: String! @index
blockNumber: Int! @index
blockTimestamp: BigInt!
expectedBlockHash: String!
reason: String!
sourceEventIds: [String!]!
resolved: Boolean! @index
resolvedCheckpointId: String
}
The reason must be a sanitized category, not a raw provider message. If a later replay or same-block trigger succeeds, mark the failure resolved and point it to the persisted checkpoint.
Do not use accountingComplete. For canonical VaultV3, totalAssets == totalDebt + totalIdle is an accounting identity/RPC-consistency check, not proof of event-history completeness.
Persist a checkpoint only when all three reads succeed and the provider's canonical block is verified. A persisted row should therefore have canonicalBlockVerified = true; retain the field so consumers can assert the contract explicitly. Persist the actual identity result without clamping or reconciliation.
Trigger invariant
Refresh the block-end checkpoint after:
Deposit
Withdraw
DebtUpdated
StrategyReported
Multiple trigger events in one block must merge into the same checkpoint. sourceEventIds contains all triggering normalized event IDs, ordered by (transactionIndex, logIndex, id) and deduplicated.
This trigger set is valid only if every successful code path that mutates total_idle or total_debt emits at least one trigger event. Audit this invariant for every supported vault release/code hash and commit the audit evidence. Include companion-event cases such as debt purchase and forced strategy revocation. A new release is not covered until this mutation audit passes.
Do not synthesize or seed a checkpoint before the certified coverage start. Before that block, history is unavailable.
Archive-RPC Effect
Use Envio createEffect and viem to read at the event block:
totalAssets()
totalDebt()
totalIdle()
Effect input/cache identity must include:
- Chain ID through
crossChain: false.
- Lowercase vault address.
- Block number.
- Expected Envio block hash.
Required Effect settings and behavior:
cache: true.
crossChain: false is mandatory.
- Enable viem transport batching where supported.
- Initial per-chain rate limit: 5 Effect executions per second.
- Per-attempt timeout: 20 seconds.
- Maximum attempts: 3 total.
- Retry only transient transport, timeout, and provider-rate-limit errors.
- Backoff: exponential base 500 ms, factor 2, maximum 2 seconds, with full jitter.
- Do not retry deterministic contract reverts, unsupported historical-state errors, or canonical-hash mismatches.
- On final failure the Effect sets
context.cache = false and throws to its handler. The handler catches that failure, persists or updates VaultAccountingCheckpointFailure, writes no checkpoint, and continues the shared indexer. Never return zeroes, partial values, or a false-success checkpoint.
Benchmark Ethereum before full replay. Record observed request rate, error rate, replay throughput, and projected duration. If measured provider limits require changing the constants above, update the constants, tests, and runbook together before continuing; do not leave runtime behavior undocumented.
Canonical-state verification
Putting a block hash in the cache key prevents cross-fork cache reuse but does not prove an eth_call by block number used that hash.
For every Effect execution:
- Read the archive provider's block hash for
blockNumber and compare it with the Envio event block hash.
- Prefer EIP-1898/block-hash-pinned state calls when supported by the provider/client.
- Otherwise perform the three reads at
blockNumber, then fetch/compare the provider block hash again.
- If either comparison fails or the hash changes around the reads, set
context.cache = false and throw.
Do not cache a result whose canonical association was not proven.
Archive configuration
Use dedicated allocation-history archive variables in the shared project; do not assume the indexer's head/watchdog RPC is archival. Suggested names:
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_ETHEREUM
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_OPTIMISM
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_GNOSIS
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_BASE
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_ARBITRUM
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_POLYGON
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_BERACHAIN
ENVIO_ALLOCATION_ARCHIVE_RPC_URL_KATANA
Only require variables for chains actually approved for allocation history. Do not add a Sonic variable. Missing allocation archive configuration must not affect unrelated chains or handlers. Update example configuration and documentation without credentials. Missing required configuration must produce a chain-specific error without logging the URL.
Gate 3 acceptance
- Same-block triggers merge deterministically.
- Reads are performed at the exact historical block.
- Cache identity includes vault, block number, expected hash, and per-chain scope.
- Provider hash mismatch before or after reads prevents persistence and caching.
- Unsupported historical state and final RPC failures emit no checkpoint.
- Accounting mismatch remains visible through
accountingIdentityHolds = false.
- The supported vault-release mutation audit proves the trigger invariant.
- A final archive-RPC failure creates an unresolved sanitized failure record, emits no checkpoint, and does not halt the shared Envio indexer. A later successful replay resolves that record.
Gate 4: coverage, query contract, parity, and rollout
Chain scope
Ethereum is required first. Additional chains may be added only when they are both:
- Explicitly enabled for allocation-history handlers and archive reads in the shared deployment; and
- Approved as required by Kong's allocation-history product.
Potential follow-on chains currently present in the shared configuration include Optimism, Gnosis, Base, Arbitrum, Polygon, Berachain, and Katana. Their presence in the shared config does not enable or certify allocation-history coverage.
Sonic is out of scope. Do not enable allocation-history handlers or archive reads for chain 146, and do not treat its absence as an acceptance blocker.
Machine-readable coverage
Expose a queryable entity equivalent to:
type VaultAllocationCoverage {
id: ID! # `${coverageRevision}:${chainId}:${vaultLower}`
chainId: Int! @index
vaultAddress: String! @index
coverageStartBlock: Int!
coverageStartBlockHash: String!
validatedThroughBlock: Int!
validatedThroughBlockHash: String!
vaultDiscoveryComplete: Boolean!
eventHistoryComplete: Boolean!
allocatorDeploymentHistoryComplete: Boolean!
allocatorAssignmentHistoryComplete: Boolean!
checkpointTriggerAuditComplete: Boolean!
safeForTimeline: Boolean!
knownGapsJson: String!
coverageRevision: String!
producerCommit: String!
validatedAt: BigInt!
}
Back this entity with a versioned, checked-in coverage manifest and deterministic publication mechanism. safeForTimeline defaults to false. Set it true only when every required fact is proven for the stated block range. A checkpoint identity match alone must never change it. Any unresolved VaultAccountingCheckpointFailure inside the claimed range prevents certification.
For each included chain/vault, the coverage evidence must pin:
- As-of block number and hash.
- Authoritative registry/factory/RoleManager discovery sources.
- Every historical Yearn V3 vault in scope, not only currently active vaults.
- Vault deployment block and Envio discovery block.
- First required indexed event.
- Allocator deployment/registration and assignment-history start blocks.
- Vault release/API version or code hash.
- Earliest safe timeline block.
- Explicit exclusions and reasons.
- Producer commit, coverage revision, and validation timestamp.
Also generate a human-readable Markdown matrix from the same manifest. The JSON/entity is authoritative; the Markdown file must not drift independently.
Incremental GraphQL query contract
Document and test the exact Hasura query Kong will use.
Scope:
chainId exactly matches the requested chain.
vaultAddress is a lowercase exact match.
- Cursor belongs to the same chain, vault, and
coverageRevision.
Ordering:
blockNumber ASC,
transactionIndex ASC,
logIndex ASC,
id ASC
The cursor is exclusive. Its continuation predicate is equivalent to:
blockNumber > cursor.blockNumber
OR (
blockNumber = cursor.blockNumber
AND transactionIndex > cursor.transactionIndex
)
OR (
blockNumber = cursor.blockNumber
AND transactionIndex = cursor.transactionIndex
AND logIndex > cursor.logIndex
)
OR (
blockNumber = cursor.blockNumber
AND transactionIndex = cursor.transactionIndex
AND logIndex = cursor.logIndex
AND id > cursor.id
)
Cursor payload contract:
{
"v": 1,
"chainId": 1,
"vaultAddress": "0x...",
"coverageRevision": "...",
"blockNumber": 123,
"transactionIndex": 4,
"logIndex": 17,
"id": "1:0x...:17"
}
Envio/Hasura queries may use these structured variables. If Kong exposes the cursor outside its internal job, Kong should encode it as an opaque versioned value.
- Default page size: 500.
- Maximum page size: 2,000.
- A cursor from another chain, vault, coverage revision, or malformed version is rejected rather than silently restarted.
- Document the exact query, variables, endpoint/authentication mode, permissions, and error behavior.
For checkpoints, document the exact query for the latest row where (chainId, vaultAddress) match and blockNumber <= targetBlock, ordered by block descending.
Tests and parity
Unit/golden tests are required but do not prove historical coverage. Add both:
- Deterministic unit and handler tests using the repository-compatible Envio 3.6 test facilities.
- A credentialed read-only parity harness that compares committed Envio fixtures with direct archive reads.
Required focused cases include:
- Every normalized event serializer and ABI variant.
- Deterministic IDs, ordering, and cursor continuation at page boundaries inside a transaction/block.
- Initial and updated allocator assignments.
- Unknown allocator role holder.
- Deployment/assignment conflict.
- Same-block dynamic registration ordering.
- Multiple checkpoint triggers in one block.
- Provider hash differs before calls.
- Provider hash changes after calls.
- Archive provider has the block but lacks historical state.
- RPC retry exhaustion produces no checkpoint.
- Different expected block hash cannot reuse a cached result.
- Accounting identity mismatch.
- Full replay determinism.
Committed exact Ethereum fixtures must include:
- A yvWETH-1 allocation block with multiple strategies.
- A yvUSDC-1 allocation block containing the historically omitted/outside-optimizer strategy.
- An
UpdateDebtAllocator assignment change, or an explicitly documented proof that no such Ethereum fixture exists in the validated range.
- A block with multiple relevant events.
- A deposit/withdraw block that changes idle.
- A report/debt-reduction block exercising loss-sensitive accounting.
Each fixture pins chain ID, addresses, block number, block hash, transaction/log ordering, expected normalized JSON, exact raw accounting values, and expected assignment/provenance results.
The credentialed parity job must run as a deployment gate or scheduled CI job against the candidate shared-deployment revision. Local runs without credentials may skip cleanly, but the report must say not run, not passed. Never print credentials or RPC URLs.
Shared-deployment rollout
- Deploy a candidate revision of the existing Envio project with fresh candidate storage while the current production revision and database remain available.
- Replay Ethereum allocation history in that candidate using a dedicated archive RPC.
- Keep
safeForTimeline = false during replay and validation.
- Verify that archive failures create gap records without stopping unrelated indexing.
- Run yvWETH/yvUSDC exact parity and cursor tests.
- Compare full replay with a fresh incremental continuation.
- Measure Effect calls, errors, unresolved gaps, replay duration, database/cache growth, and GraphQL latency.
- Certify the Ethereum coverage manifest and only then set eligible vaults
safeForTimeline = true in a new immutable coverage revision.
- Allow Kong to integrate only against that certified revision.
- Cut over to the candidate so that one shared Envio server remains active. Retain the previous deployment revision as the rollback target.
- Add another approved chain only by repeating its discovery, ABI, mutation, archive, fixture, and parity audits.
This rollout must not create a second permanent Envio project or database. A temporary candidate revision and fresh candidate storage are allowed for safe replay and validation.
Gate 4 acceptance
- Ethereum coverage is machine-readable and evidence-backed.
safeForTimeline remains false for incomplete or unaudited vault ranges and for any range with unresolved archive-read failures.
- Exact event/checkpoint queries are documented and tested.
- Credentialed yvWETH/yvUSDC parity passes against the deployed candidate.
- Full and incremental ingestion produce equivalent results for the same cutoff.
- Failure/recovery and backout procedures are documented.
- Any additional chain has separately passed the same gate.
Global non-goals
Do not implement:
- Sonic/chain 146 allocation indexing.
VaultAllocationTimeline.
- Allocation transitions or
effects[] grouping.
- Actor-role classification beyond providing top-level inputs.
- Traces or nested Safe/router/multicall decoding.
- DOA ingestion, matching, or aging.
- Redis blobs or Kong REST routes.
- Strategy names/metadata.
- Bps or synthetic
Unallocated strategies.
- Uniform timeseries sampling.
- Per-strategy historical RPC reads.
- Full allocator execution-feasibility/configuration history.
- yearn.fi or Powerglove changes.
- Production cutover to Kong as part of this implementation issue.
Verification and final report
Run and report for the shared deployable project:
- Envio code generation.
- TypeScript build/typecheck.
- Config compatibility checks.
- Full existing test suite.
- Focused allocation tests.
- Lint/format checks.
git diff --check.
- Ethereum replay benchmark.
- Credentialed deployed-candidate parity status.
The final report must distinguish:
- Implemented locally.
- Unit/golden verified.
- Credentialed parity verified.
- Deployed but not certified.
- Certified
safeForTimeline coverage.
- Remaining gaps and owner decisions.
Do not report the feature complete solely because schema generation, build, mock tests, or an HTTP 200 succeeds.
Vault Allocation History producer contract
Implement the Envio producer-side contract required by Kong's planned Vault Allocation History feature.
This specification supersedes the previous issue body. It deliberately separates deployment provenance from active allocator assignment, distinguishes accounting identity from historical completeness, and prevents archive-RPC failures from halting the shared Yearn Envio indexer.
Decisions
chainId = 146) is explicitly out of scope and does not need to be indexed, audited, configured, or documented as a gap for this feature.0xstrings. Checksum formatting is a presentation concern.Objective and ownership boundary
The shared Envio deployment must expose:
totalAssets,totalDebt, andtotalIdlecheckpoints at accounting-changing blocks.Kong will consume these primitives to build:
effects[].VaultAllocationTimelineresponses.Envio must not implement Kong's finished timeline, DOA logic, transition classifications, public REST route, or Redis cache.
Do not add per-strategy archive reads. Kong can replay strategy state from events:
DebtUpdated.new_debtandStrategyReported.current_debt.StrategyChanged.StrategyReported.blockTimestamp.UpdatedMaxDebtForStrategy.UpdateStrategyDebtRatios.Single-deployment and failure-domain requirement
Implement allocation history inside the existing Envio project. It uses one:
The allocation feature is Ethereum-only for its first production candidate, even though the shared indexer supports several chains. Allocation handlers and archive calls must therefore have an explicit chain guard. Existing non-allocation entities and handlers must keep their current multichain behavior.
Archive-RPC reads still fail closed for checkpoint data: never write zeroes, partial values, or an unverified checkpoint. However, a final archive-RPC failure must be caught by the allocation handler. It must write a sanitized, queryable failure record, leave the checkpoint absent, keep
safeForTimeline = false, and allow the shared indexer to continue. Never store credentials or RPC URLs in the failure record.Envio's
crossChain: falsesetting remains required because it isolates Effect cache and rate-limit budgets by chain. It does not provide process isolation, so the handler-level failure boundary is mandatory.Adding persisted event configuration is not compatible with resuming the currently initialized database. The rollout must therefore use fresh candidate storage for the full replay, keep current production available during validation, and leave one active deployment/database after cutover.
Delivery gates
Implement and validate these in order. Do not claim the full issue complete when an earlier gate passes.
Each gate must retain focused unit/golden tests and evidence in the final report.
Gate 1: normalized allocation-event contract
Required event scope
V3 vault
DepositWithdrawDebtUpdatedStrategyReportedStrategyChangedUpdatedMaxDebtForStrategyDebtPurchasedUpdateDefaultQueueUpdateUseDefaultQueueUpdateMinimumTotalIdleUpdateAutoAllocateShutdownRoleSetRoleStatusChangedUpdateRoleManagerUpdateAccountantDepositandWithdraware required because idle-only flows change allocation percentages.UpdateMinimumTotalIdleandUpdateAutoAllocatedescribe intentional idle policy;Shutdownchanges future allocation behavior.RoleManager
AddedNewVaultRemovedVaultUpdateDebtAllocatorDebt allocator factory
NewDebtAllocatorSupport each ABI variant actually deployed on an included chain. Do not assume an event name or topic0 proves that indexed/non-indexed parameter layout is identical.
Generic debt allocator
UpdateStrategyDebtRatiosUpdateKeeperGovernanceTransferredOperational feasibility settings such as minimum change/wait, maximum acceptable base fee, maximum accepted update loss, pause state, and manager configuration are non-goals for the first contract. Add them only in a separately approved expansion.
Entity contract
Add an entity equivalent to:
Allowed
sourceTypevalues:vaultroleManagerdebtAllocatorFactorydebtAllocatorRequirements:
${chainId}:${transactionHashLower}:${logIndex}.signatureis the lowercase event topic0.0xencoding.topLevelInputSelectoris exactly the first four calldata bytes (0xplus eight hex characters), otherwise null.toandinputonly for the allocation project's event set; do not expand the primary indexer's global transaction payload.normalizationVersion = 1initially.abiVariantidentifies the audited event layout/release/code-hash family when variants exist; it is null only when there is exactly one proven layout.The normalized
argsJsonkeys for normalization version 1 are fixed as follows. Key order is exactly the order shown:Depositsender,owner,assets,sharesWithdrawsender,receiver,owner,assets,sharesDebtUpdatedstrategy,currentDebt,newDebtStrategyReportedstrategy,gain,loss,currentDebt,protocolFees,totalFees,totalRefundsStrategyChangedstrategy,changeTypeUpdatedMaxDebtForStrategysender,strategy,newDebtDebtPurchasedstrategy,amountUpdateDefaultQueuenewDefaultQueueUpdateUseDefaultQueueuseDefaultQueueUpdateMinimumTotalIdleminimumTotalIdleUpdateAutoAllocateautoAllocateShutdown{}RoleSetaccount,roleRoleStatusChangedrole,statusUpdateRoleManagerroleManagerUpdateAccountantaccountantAddedNewVaultvault,debtAllocator,categoryRemovedVaultvaultUpdateDebtAllocatorvault,debtAllocatorNewDebtAllocatorallocator,vault, followed byoriginalAllocatoronly for a proven ABI variant that emits itUpdateStrategyDebtRatiosstrategy,newTargetRatio,newMaxRatio,newTotalDebtRatioUpdateKeeperkeeper,allowedGovernanceTransferredpreviousGovernance,newGovernanceAll integer-valued fields in this table, including enum-like values such as
changeType,role,status, andcategory, are decimal strings. Address arrays preserve emitted order. ABI variants with different indexed layouts normalize to the same keys;abiVariantidentifies which decoder produced them. Do not add or rename keys without incrementingnormalizationVersion.Versioned serializer contract
Do not serialize
event.paramsgenerically. Define an explicit serializer per supported event and ABI variant. For every serializer, document and golden-test:0xstrings.argsJsonmust be deterministic byte-for-byte for identical input. It must not contain arbitrary RPC responses.Gate 1 acceptance
Gate 2: allocator provenance and assignment history
NewDebtAllocatoris deployment provenance, not complete active-assignment history. Model the facts separately.Deployment binding
Populate this immutable binding from
NewDebtAllocator. If an ABI variant does not emitoriginalAllocatorAddress, leave it null and identify the variant; do not infer it.RoleManager assignment history
AddedNewVaultcreates the initial assignment.UpdateDebtAllocatorcreates later assignments.RemovedVaultcloses RoleManager membership but must not delete historical assignments.If a recognized factory deployment binding contradicts a RoleManager assignment, surface the disagreement in a queryable conflict entity or explicit assignment status. Never overwrite one fact with the other.
Dynamic-registration same-block requirement
Envio may retrospectively return logs earlier in the same block from a dynamically registered contract. Before using immediate allocator-to-vault lookup with hard failure:
NewDebtAllocatorin the registration block.If that invariant cannot be proven, implement deterministic block-level/deferred association. In either case:
Gate 2 acceptance
Gate 3: historical vault accounting checkpoints
Checkpoint contract
Also expose unresolved and resolved archive-read failures:
The reason must be a sanitized category, not a raw provider message. If a later replay or same-block trigger succeeds, mark the failure resolved and point it to the persisted checkpoint.
Do not use
accountingComplete. For canonical VaultV3,totalAssets == totalDebt + totalIdleis an accounting identity/RPC-consistency check, not proof of event-history completeness.Persist a checkpoint only when all three reads succeed and the provider's canonical block is verified. A persisted row should therefore have
canonicalBlockVerified = true; retain the field so consumers can assert the contract explicitly. Persist the actual identity result without clamping or reconciliation.Trigger invariant
Refresh the block-end checkpoint after:
DepositWithdrawDebtUpdatedStrategyReportedMultiple trigger events in one block must merge into the same checkpoint.
sourceEventIdscontains all triggering normalized event IDs, ordered by(transactionIndex, logIndex, id)and deduplicated.This trigger set is valid only if every successful code path that mutates
total_idleortotal_debtemits at least one trigger event. Audit this invariant for every supported vault release/code hash and commit the audit evidence. Include companion-event cases such as debt purchase and forced strategy revocation. A new release is not covered until this mutation audit passes.Do not synthesize or seed a checkpoint before the certified coverage start. Before that block, history is unavailable.
Archive-RPC Effect
Use Envio
createEffectand viem to read at the event block:totalAssets()totalDebt()totalIdle()Effect input/cache identity must include:
crossChain: false.Required Effect settings and behavior:
cache: true.crossChain: falseis mandatory.context.cache = falseand throws to its handler. The handler catches that failure, persists or updatesVaultAccountingCheckpointFailure, writes no checkpoint, and continues the shared indexer. Never return zeroes, partial values, or a false-success checkpoint.Benchmark Ethereum before full replay. Record observed request rate, error rate, replay throughput, and projected duration. If measured provider limits require changing the constants above, update the constants, tests, and runbook together before continuing; do not leave runtime behavior undocumented.
Canonical-state verification
Putting a block hash in the cache key prevents cross-fork cache reuse but does not prove an
eth_callby block number used that hash.For every Effect execution:
blockNumberand compare it with the Envio event block hash.blockNumber, then fetch/compare the provider block hash again.context.cache = falseand throw.Do not cache a result whose canonical association was not proven.
Archive configuration
Use dedicated allocation-history archive variables in the shared project; do not assume the indexer's head/watchdog RPC is archival. Suggested names:
Only require variables for chains actually approved for allocation history. Do not add a Sonic variable. Missing allocation archive configuration must not affect unrelated chains or handlers. Update example configuration and documentation without credentials. Missing required configuration must produce a chain-specific error without logging the URL.
Gate 3 acceptance
accountingIdentityHolds = false.Gate 4: coverage, query contract, parity, and rollout
Chain scope
Ethereum is required first. Additional chains may be added only when they are both:
Potential follow-on chains currently present in the shared configuration include Optimism, Gnosis, Base, Arbitrum, Polygon, Berachain, and Katana. Their presence in the shared config does not enable or certify allocation-history coverage.
Sonic is out of scope. Do not enable allocation-history handlers or archive reads for chain 146, and do not treat its absence as an acceptance blocker.
Machine-readable coverage
Expose a queryable entity equivalent to:
Back this entity with a versioned, checked-in coverage manifest and deterministic publication mechanism.
safeForTimelinedefaults to false. Set it true only when every required fact is proven for the stated block range. A checkpoint identity match alone must never change it. Any unresolvedVaultAccountingCheckpointFailureinside the claimed range prevents certification.For each included chain/vault, the coverage evidence must pin:
Also generate a human-readable Markdown matrix from the same manifest. The JSON/entity is authoritative; the Markdown file must not drift independently.
Incremental GraphQL query contract
Document and test the exact Hasura query Kong will use.
Scope:
chainIdexactly matches the requested chain.vaultAddressis a lowercase exact match.coverageRevision.Ordering:
The cursor is exclusive. Its continuation predicate is equivalent to:
Cursor payload contract:
{ "v": 1, "chainId": 1, "vaultAddress": "0x...", "coverageRevision": "...", "blockNumber": 123, "transactionIndex": 4, "logIndex": 17, "id": "1:0x...:17" }Envio/Hasura queries may use these structured variables. If Kong exposes the cursor outside its internal job, Kong should encode it as an opaque versioned value.
For checkpoints, document the exact query for the latest row where
(chainId, vaultAddress)match andblockNumber <= targetBlock, ordered by block descending.Tests and parity
Unit/golden tests are required but do not prove historical coverage. Add both:
Required focused cases include:
Committed exact Ethereum fixtures must include:
UpdateDebtAllocatorassignment change, or an explicitly documented proof that no such Ethereum fixture exists in the validated range.Each fixture pins chain ID, addresses, block number, block hash, transaction/log ordering, expected normalized JSON, exact raw accounting values, and expected assignment/provenance results.
The credentialed parity job must run as a deployment gate or scheduled CI job against the candidate shared-deployment revision. Local runs without credentials may skip cleanly, but the report must say not run, not passed. Never print credentials or RPC URLs.
Shared-deployment rollout
safeForTimeline = falseduring replay and validation.safeForTimeline = truein a new immutable coverage revision.This rollout must not create a second permanent Envio project or database. A temporary candidate revision and fresh candidate storage are allowed for safe replay and validation.
Gate 4 acceptance
safeForTimelineremains false for incomplete or unaudited vault ranges and for any range with unresolved archive-read failures.Global non-goals
Do not implement:
VaultAllocationTimeline.effects[]grouping.Unallocatedstrategies.Verification and final report
Run and report for the shared deployable project:
git diff --check.The final report must distinguish:
safeForTimelinecoverage.Do not report the feature complete solely because schema generation, build, mock tests, or an HTTP 200 succeeds.