Skip to content

feat(vault-v2-reallocation): migrate vault v2 reallocation bot - #166

Open
haydenshively wants to merge 11 commits into
feat/blue-reallocation-botfrom
feat/vault-v2-reallocation-bot
Open

feat(vault-v2-reallocation): migrate vault v2 reallocation bot#166
haydenshively wants to merge 11 commits into
feat/blue-reallocation-botfrom
feat/vault-v2-reallocation-bot

Conversation

@haydenshively

@haydenshively haydenshively commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #165. Migrates the standalone vault-v2-reallocation-bot into the monorepo as bots/vault-v2-reallocation, the sibling of bots/vault-v1-reallocation, on the same flat bots-as-programs shape and @repo/bot-kit runtime.

What's here

  • GraphQL dropped for RPC, block-pinned: vault state comes from blue-sdk's fetchAccrualVaultV2 (factory-validated — a free per-tick V2 identity check) plus per-id absoluteCap/relativeCap/allocation reads the SDK fetcher doesn't cover for regular adapters. The old bot's graphql-request + codegen SDK + .gql files and its checked-in 1754-line ABI are gone; zero new external dependencies (blue-sdk 6.5.0 was already pinned for the V1 bot).
  • Both strategies ported (equalize-utilizations — the default, what prod ran — and apy-range) as config-injected closures emitting exact-amount deltas; idle is first-class (denominator folding / idle top-up), executed as ONE vault.multicall([deallocate…, allocate…]).
  • One shared-package change (first commit): a declarative Policy.multicall { innerSelectors, innerTargetsByOuter } field — evaluatePolicy itself decodes the bundle and requires every inner call to be allocate/deallocate to the adapter registered for the targeted vault. No bot-supplied code runs inside the guard, so the outer-envelope trust boundary is unchanged (hence no TIB; this is the V2 analogue of feat(vault-v1-reallocation): migrate vault v1 reallocation bot #165's target-list widening). Tested incl. smuggled-selector, cross-vault-adapter, empty-bundle, and malformed-bytes denials.
  • All three cap levels enforced in sizing (upgrade over the old bot, which ignored adapter/collateral caps): per-market cap headroom plus shared adapter-level and per-collateral pools, all measured against the on-chain allocation(id) the contract enforces — NOT accrued position assets, which drift above it (visible in live data: allocation 120000000000 vs accrued 120001419437 on Hyperithm Apex). A binding aggregate cap shrinks the plan instead of looping sim-reverts.
  • Both adapter generations supported: every live VaultV2 uses the MorphoMarketV1AdapterV2 contract, not the original adapter the old bot's code nominally modeled — vault-data.ts normalizes both SDK shapes (identical calldata + cap ids). Fail-loud typed error unless a vault has exactly one Morpho Blue market adapter.
  • Same runtime/safety surface as the V1 bot: signer policy pinned to whitelist+multicall, pending queue, per-tick allocator re-check with auto-resume, DRY_RUN ramp-up (compose/Railway default new services to it), balance/heartbeat monitors, wide tick.end counters, operator surface + CI wiring (vault-v2-realloc in deploy-bot/staging/production + CalVer release job).

⚠️ Found during verification: the old bot's prod whitelist is broken

All three whitelisted addresses in the old repo's production config (mainnet 0xBEEF0173…, 0x8eB67A50…; base 0xbeeF010f…) are MetaMorpho V1 vaults, not VaultV2s (verified on-chain: withdrawQueueLength answers, adaptersLength reverts, the V2 factory disowns them) — the old V2 bot cannot ever have reallocated anything in production. Deploy-time whitelists must be chosen fresh; the README documents this and this bot fails loud at startup on any non-V2 entry.

Deliberately dropped from the old repo

GraphQL API layer, checked-in VaultV2 ABI, capsIds.ts (SDK helpers are byte-identical), hand-rolled WAD/IRM math (blue-sdk MathLib/AdaptiveCurveIrmLib/SharesMath), multi-chain single process, setInterval runtime, Promise.all sends, console logging, the dead apy-range override tables (their keys never matched the whitelist), and the anvil fork suites (the old vaultSetup.ts V2 timelock harness is the seed for a future fork suite; noted in README).

Verification

  • pnpm lint / pnpm format / pnpm knip / typecheck (bot + bot-kit): clean. Root pnpm test: all pass except the 4 pre-existing fork/e2e suites needing RPC_URL_8453 env files. 63 new bot tests + 7 new bot-kit policy tests; break-one-assertion verified per repo rule; quoter-bot jsdoc:build green.
  • Live smoke on mainnet (public RPC, throwaway key, DRY_RUN=true) against Hyperithm USDC Apex (~$8.5M, 20 markets): startup identity/adapter/policy wiring, allocator.missing_role skip path, interval gate, clean tick.end counters and SIGINT. A separate live probe exercised fetchVaultV2Data → strategy → (no move found) with real caps/allocations.
  • The fail-loud paths were exercised live: non-V2 address → startup.error factory rejection; V2 vault with an unsupported adapter shape → typed InvalidVaultError.
  • docker build not verified locally (daemon not running) — the Dockerfile is the V1 bot's byte-for-byte with paths adjusted; Railway builds server-side.

Follow-ups

Shared with the V1 bot: fork test suite, pure-bigint bips gate cleanup, possible extraction of the shared strategy math into a @repo/* package once both bots merge. Railway provisioning + GitHub Environments (vault-v2-realloc-{staging,production}) are deploy-time operator steps.

🤖 Generated with Claude Code

Update: restacked on #165's review-fix commit (61980bc)

Mirrored the applicable V1 review fixes in fix(vault-v2-reallocation): mirror v1 review fixes: bot-kit's new simulateCall replaces the local simulate helper; snapshots accrue to the pinned block's timestamp instead of wall clock; apy-range hard-excludes non-AdaptiveCurve markets (a zero rateAtTarget degenerates the curve inversion into 'far below range' and would drain the position on simulation-passing calldata — same hazard the V1 review found), surfaced per tick as market.non_adaptive_curve; equalize clamps its target utilization at 100% for bad-debt states. Deliberately NOT mirrored: the V1 allocator|curator|owner role-gate widening — VaultV2.allocate requires isAllocator[msg.sender] strictly (vaults-v2 source, ALLOCATOR FUNCTIONS), so curator/owner keys do not qualify on V2.

Update: dual-model review pass (Opus + GPT 5.6 Sol) — 3 must-fix correctness defects found and fixed

Commit fix(vault-v2-reallocation): apply dual-model review findings:

  1. relativeCap == WAD is the contract's no-constraint sentinel (allocateInternal skips the relative check) — the bot treated it as a binding 100% ceiling, zeroing the adapter pool on any fully-deployed vault → silent permanent no-op. Confirmed live: Hyperithm USDC Apex produced no plan pre-fix and an exactly-funded 9-leg plan post-fix. The old test fixtures (allocation: 0 aggregates) masked the production shape; new tests pin it.
  2. Cap headroom basis corrected to the accrued position (the adapter trues allocation(id) up to expectedSupplyAssets on every touch, so cap − storedAllocation overstates headroom by the drift), with aggregate pools carrying every market's drift and the plan's own deallocations credited back (they execute first) — a full adapter cap no longer blocks valid rebalances.
  3. Equalize now clamps allocations to deallocations + idle (allocate pulls from vault balance; unclamped plans were guaranteed revert-loops on real shapes, e.g. a small adapter position in a big cold market).

Plus should-fixes/nits from the same pass and a peer-session audit: zero-target division guards (dust borrow / sub-curve APY bounds), contribution-gated min-delta triggers, classifyMarket/apyDeltaBips helpers de-duplicating the two passes, throw-safe multicall policy evaluation + dirty-padding test in bot-kit, adapter.changed tick detection for mid-run adapter swaps, market ids on reallocation.found legs, multicall-batched cap reads + parallel getBlock, isAddressEqual conventions, stale V1 wording, and honest README language for the both-sides leg gate (see below).

Known behavior kept for old-bot fidelity (deliberate, flagged for product review): a plan fires only when both sides have ≥1 leg, so pure idle deployment (fresh deposits, every market at/above target) never fires — same gate as the old V2 bot, but in V1 idle was a market so the gate had different reach. Documented in the README as a follow-up decision.

Verification: 303 tests green across vault-v2-reallocation/bot-kit/vault-v1-reallocation/workspace; break-one-assertion on the WAD sentinel, idle clamp, zero-target guards, and pool crediting; live probe on a $8.5M 20-market prod vault validating alloc ≤ dealloc + idle and plan encoding.

@haydenshively
haydenshively force-pushed the feat/vault-v2-reallocation-bot branch 4 times, most recently from d5e7bb6 to 03a8fa7 Compare August 17, 2026 17:48
@haydenshively haydenshively self-assigned this Aug 17, 2026
@haydenshively
haydenshively marked this pull request as ready for review August 17, 2026 18:09
@haydenshively
haydenshively requested a review from cashd August 17, 2026 18:09

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

function required(env: Env, name: string): string {
const value = env[name]
if (!value || !value.trim()) throw new Error(`Missing required env var: ${name}`)
return value.trim()

P1 Badge Replace plain deployment errors with typed failures

When required configuration is absent—and throughout the other expected Railway CLI failure paths—this script throws plain Error, preventing callers from distinguishing configuration, CLI, and deployment failures and bypassing the repository’s required sanitization boundary. Define named exported error subclasses in separate kebab-case *.error.ts files and use the appropriate type at each expected failure site.

AGENTS.md reference: AGENTS.md:L43-L50


function required(env: Env, name: string): string {
const value = env[name]
if (!value || !value.trim()) throw new Error(`Missing required env var: ${name}`)
return value.trim()

P1 Badge Convert deployment utilities to arrow constants

This newly added deployment script declares required and the remaining helper utilities with function declarations, contrary to the repository-wide arrow-utility convention. Convert these helpers to const arrow functions so the new operator surface follows the mandated utility pattern.

AGENTS.md reference: AGENTS.md:L51-L52


const decodeMulticallData = (data: Hex): readonly Hex[] | undefined => {

P1 Badge Move multicall utilities out of the error-class module

The newly added decodeMulticallData and checkMulticall utilities live in policy.ts, which also contains PolicyViolationError; this breaks the repository’s utility-isolation invariant. Move the multicall helpers to a focused utility module and import them into the policy module.

AGENTS.md reference: AGENTS.md:L41-L42


const key = getAddress(collateralToken)
pools.adapter += amount
pools.byCollateral.set(key, (pools.byCollateral.get(key) ?? 0n) + amount)

P1 Badge Repay aggregate cap deficits before crediting deallocations

When an adapter or collateral allocation is already above its buffered cap—because interest accrued or a curator reduced the cap—createDepositPools starts that pool at zero, but creditPools then treats every deallocated asset as fresh headroom. A subsequent allocation can therefore restore the pre-deallocation over-cap balance, causing the exact multicall to revert repeatedly, or erase the intended 99.99% safety buffer. Track each pool’s existing cap deficit and credit only the portion of a deallocation that moves the effective allocation below the buffered ceiling.


MathLib.min(
getWithdrawalToUtilization(marketData.state, targetUtilization),
marketData.vaultAssets

P2 Badge Preserve a position in original-generation adapter markets

When a market on the supported original MorphoMarketV1Adapter has zero or sufficiently low utilization, the computed withdrawal can be at least the adapter’s entire position, and this clamp emits a full deallocation. That adapter removes a market from marketParamsList when its allocation reaches zero, while this bot discovers candidates from that list, so later ticks cannot re-enter the market if demand returns without an external allocator first restoring it. Cap the withdrawal below the full position for this adapter generation or discover inactive markets independently.


// Config/client never came up, so we cannot honor LOG_LEVEL — emit the failure directly.
console.error(
JSON.stringify({ level: 'error', event: 'startup.error', error: ensureError(error).message })
)

P1 Badge Sanitize startup errors before writing them to logs

When startup fails during an RPC-backed vault check, a viem transport error’s full message can contain the authenticated RPC URL, request payload, and provider response, and this catch writes it verbatim to operator logs. Emit a bounded safe reason such as the viem shortMessage, while retaining the original error only as an internal cause, so provider credentials and untrusted response data cannot leak.

AGENTS.md reference: AGENTS.md:L48-L50


const strategy = suffixed('STRATEGY', chain.chainId) ?? 'equalize-utilizations'
// New deployments default to dry-run; flip DRY_RUN_<chainId>=false once the plans look right.
const dryRun = !/^(0|false)$/i.test(suffixed('DRY_RUN', chain.chainId) ?? '')
const betterstackHeartbeatUrl = suffixed('BETTERSTACK_HEARTBEAT_URL', chain.chainId)

P2 Badge Propagate per-chain reallocation intervals during deployment

When operators follow the documented migration posture and supply REALLOCATION_INTERVAL_MS_1=900000 and REALLOCATION_INTERVAL_MS_8453=300000, this deployment script never reads or writes either value, so both Railway services silently run the bot’s 600000 ms default instead of the stated production cadences. Parse the suffixed interval with the other per-chain settings and set REALLOCATION_INTERVAL_MS on each service.


const toDeallocate = min(move.amount, remainingAmountToDeallocate)
remainingAmountToDeallocate -= toDeallocate
creditPools(legPools, move.marketData.params.collateralToken, toDeallocate)
if (toDeallocate > 0n) deallocations.push(toLeg(move, toDeallocate))

P2 Badge Preserve collateral funding when trimming deallocations

When idle parking is disabled and deallocations from multiple collateral groups exceed the allocation total, this market-order trim can retain a deallocation from collateral A while dropping the collateral B deallocation whose credit made the planned B allocation pass sizing. The emission pass then clamps the B allocation to zero but still returns and submits the retained deallocation, parking assets idle despite allowIdleParking: false. Trim deallocations with awareness of the collateral pools required by emitted allocations, or discard/rebalance the plan when the final sides no longer match.


if (min(totalAmountToDeallocate, totalAmountToAllocate) === 0n || !didClearMinDelta) {
return undefined

P2 Badge Re-evaluate the delta threshold after trimming legs

When the only move with clearsMinDelta: true is later omitted by the market-order budget or cap-pool trimming, this pre-emission check remains armed and the reconciler submits only below-threshold legs. That defeats the strategy’s firing threshold and can generate repeated low-value transactions; compute the gate from the legs that actually survive into the final reallocation.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@haydenshively
haydenshively force-pushed the feat/vault-v2-reallocation-bot branch from 03a8fa7 to f9ed458 Compare August 17, 2026 19:59

@cashd cashd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

approved for OSS as a reference

@cashd cashd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

/simplify + review pass on this one. 30-ish threads below, roughly by weight — a few real ones up top (relative-cap rounding, 3 vacuous tests, README flag described backwards), then rule hits, reuse of stuff we already have in blue-sdk / @repo/utils, per-tick RPC waste, and simplifications. Anything tagged "low pri" / "not for this PR" / "fyi" is optional; I'll ticket the cross-bot dedupe and the workflow release-job copy separately.

Suggestions are inline where the drop-in was clean.

Comment thread bots/vault-v2-reallocation/src/math.ts Outdated
const bufferedAbsolute = MathLib.wMulDown(cap.absolute, capBufferWad)
const absoluteHeadroom = bufferedAbsolute > basis ? bufferedAbsolute - basis : 0n
if (cap.relative === MathLib.WAD) return absoluteHeadroom
const bufferedRelative = MathLib.wMulDown(MathLib.wMulUp(totalAssets, cap.relative), capBufferWad)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The contract rounds this down (firstTotalAssets.mulDivDown(relativeCap, WAD) in VaultV2), so wMulUp can put our ceiling 1 wei above the contract's. The 99.99% buffer hides it in prod, but the tests call this with capBufferWad = WAD where the mismatch is real.

Suggested change
const bufferedRelative = MathLib.wMulDown(MathLib.wMulUp(totalAssets, cap.relative), capBufferWad)
const bufferedRelative = MathLib.wMulDown(MathLib.wMulDown(totalAssets, cap.relative), capBufferWad)

rateAtTarget: RATE_AT_TARGET
})
const result = strategy(makeVaultData([badDebtMarket, coldMarket]))
if (result) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This passes vacuously if the strategy ever returns undefined here — every expect is inside the if. Same at L248. Both produce a plan today (I checked), but that's incidental.

Swap to expect(result).toBeDefined() and assert on result!.

expect(allocation.assets).toBeGreaterThan(0n)
})

it('folds idle assets into the target-utilization denominator', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both assertions here are toBeUndefined(), so it passes identically whether idle is folded or not. Worth a hot+cold pair asserting the allocation grows by exactly min(surplus, idle) between idleAssets: 0n and a large idle.

Comment on lines +36 to +37
AdaptiveCurveIRM curve; allocations top up from idle (`ALLOW_IDLE_REALLOCATION`). Fires only
past `MIN_APY_DELTA_BIPS`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is backwards vs the code — allowIdleParking gates whether deallocation surplus may park in idle (reconcile.ts:119); allocations always draw on idle. An operator flipping it to stop idle deployment gets nothing.

Suggested change
AdaptiveCurveIRM curve; allocations top up from idle (`ALLOW_IDLE_REALLOCATION`). Fires only
past `MIN_APY_DELTA_BIPS`.
AdaptiveCurveIRM curve; deallocation surplus parks in idle only when `ALLOW_IDLE_REALLOCATION=true`
(allocations always draw on idle). Fires only
past `MIN_APY_DELTA_BIPS`.

Comment on lines +53 to +54
const bound =
utilization > upperBound ? upperBound : utilization < lowerBound ? lowerBound : undefined

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nested ternary — repo rule is none. Early returns read cleaner:

Suggested change
const bound =
utilization > upperBound ? upperBound : utilization < lowerBound ? lowerBound : undefined
const boundFor = (u: bigint): bigint | undefined => {
if (u > upperBound) return upperBound
if (u < lowerBound) return lowerBound
return undefined
}
const bound = boundFor(utilization)

Comment on lines +60 to +64
export const getCapHeadroom = (
cap: CapState,
basis: bigint,
totalAssets: bigint,
capBufferWad: bigint

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

low pri: 4 positional params here, getDepositableAmount, intEnv, resolveMinApyDeltaBips — conventions say >3 → trailing object. Churn for little value rn, up to you.

@@ -0,0 +1,217 @@
import type { LogLevel } from '@repo/bot-kit'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Zooming out: strategy-config.ts, build.ts, both script *.error.ts, invalid-config.error.ts, tsconfig.json are byte-identical to vault-v1; this file and deploy-railway.ts differ only in strings; the env parsers here are the 5th copy repo-wide. Drift is already visible (v2's zero-target guard isn't in v1).

Not for this PR — I'll file a ticket for env parsers → bot-kit and a shared reallocation core.

gh release create "$tag" --target "$SHA" --title "$tag" --generate-notes \
${prev:+--notes-start-tag "$prev"}

Release-vault-v2-realloc:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

6th verbatim copy of this job. Not for this PR, but the tag step belongs in deploy-bot.yml behind stage == 'production' so adding a bot is one case line. Will ticket.

@@ -0,0 +1,164 @@
# vault-v2-reallocation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The v1 TIB explicitly defers V2 "as a separate decision", and this PR also changes bot-kit's documented outer-envelope-only signer guarantee (multicall inner-call inspection). I think it needs a TIB-2026-08-17-vault-v2-reallocation-bot.md covering: the multicall policy design, delta legs, both-sides-required firing, 3-level cap pools, one-adapter assumption.

const adapter = marketAdapters[0]!
const adapterAddress = getAddress(adapter.address)

const adapterMarkets = normalizeAdapterMarkets(adapter, timestamp)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

fyi only: vaultV2.accrueInterest(timestamp) at L234 re-runs position/market.accrueInterest for every market via adapter.realAssets (blue-sdk 6.4 adapters), so each market is accrued twice per vault. CPU-only, sub-ms — no change needed, just so it's known.

@haydenshively
haydenshively force-pushed the feat/vault-v2-reallocation-bot branch from 4a124fc to 603ce33 Compare August 18, 2026 05:17
haydenshively and others added 11 commits August 18, 2026 00:44
Optional declarative Policy.multicall: calldata must decode as a non-empty
multicall(bytes[]) whose inner selectors are allowed and whose inner calls'
first address argument is registered for the outer target. Policy remains
data-driven default-deny logic — no bot-supplied code runs inside the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RPC-only block-pinned vault data via blue-sdk fetchAccrualVaultV2 (+ per-id
cap/allocation reads), both strategies as delta-emitting closures with
three-level cap pools, multicall encoding, simulation, dep-injected tick,
and the bot-kit-wired entrypoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ations

All live VaultV2s use the MorphoMarketV1AdapterV2 contract; both generations
take identical allocate/deallocate calldata and cap ids, so vault-data
normalizes the two SDK adapter shapes into one market list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopt bot-kit simulateCall (local simulate helper dropped), accrue snapshots
to the pinned block's timestamp, exclude non-AdaptiveCurve markets from
apy-range (a zero rateAtTarget degenerates the inversion and would drain the
position on sim-passing calldata), and clamp equalize's target utilization at
100% for bad-debt states. The V1 role-gate widening is deliberately NOT
mirrored: VaultV2.allocate requires isAllocator strictly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cap math corrected against the VaultV2 contract: relativeCap == WAD honored
as the no-constraint sentinel (a fully-deployed vault previously zeroed its
adapter pool and silently no-opped forever); headroom measured from the
accrued position the adapter trues allocation(id) up to, with aggregate
pools carrying every market's accrual drift; the plan's own deallocations
(executed first) credit capacity back to the pools. Equalize now clamps
allocations to deallocations + idle (allocate pulls from vault balance).
Strategies restructured deallocate-first; legs carry the market id for log
correlation; the tick surfaces a mid-run adapter swap as adapter.changed
instead of opaque policy violations; policy evaluation deny-wraps any throw
from the multicall check and gains a dirty-upper-bits padding test; stale V1
wording and README claims corrected. Verified live: a fully-deployed prod
vault that previously produced no plan now emits an exactly-funded one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pass

capBufferWad converted once at strategy construction, dead MarketState share
fields dropped, reallocation.dry_run payload slimmed to the vault (the plan
is already in reallocation.found).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…econciler

Mirrors the v1 framing: a strategy answers, per market, where it should sit
and whether the move clears its min-delta threshold; one reconciler owns all
mechanics — sizing, three-level cap pools with the dealloc-first credit rule
as one explicit step, idle-balance netting (alloc ≤ dealloc + idle; dealloc
excess parks or clamps per allowIdleParking), the contributing-markets gate,
budget trim in market order, and delta-leg emission. Every pre-existing
strategy test passes unmodified; 8 reconciler-direct tests added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typed RailwayDeploymentError replaces plain Errors in the deploy script with
untrusted CLI stderr retained only as cause (setSecret stays detail-free);
deploy helpers become arrow constants with viem key validation; compose
forwards every documented runtime knob; intEnv rejects unsafe integers; the
reconciler's min-delta gate is evaluated on the TRIMMED legs so a fully
trimmed-out clearing market cannot authorize a plan of sub-threshold legs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilization

A WAD target — an APY bound past the curve max on a decayed-rateAtTarget
cold market, or a bad-debt aggregate at/above 100% — sizes a deallocation to
the market's entire free liquidity: exact to the snapshot, unrealizable one
accrual later, a sim-passes-then-reverts loop. The reconciler now clamps
every classifier target to MAX_TARGET_UTILIZATION (99.9%, ~10 bips of
borrow-side accrual margin), replacing equalize's WAD clamp; feasibility is
the reconciler's job, not the classifiers'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mandatory bot-kit consumers: Policy.targets rename and boolean queue.submit
(submitted counted only on real broadcasts, reallocation.not_broadcast on
refusal). Read client opts into batched JSON-RPC for the per-cap-id fan-out.
Whitelist dedupes case-variant addresses; vaults process concurrently via
allSettled with per-vault counter folding; the role read runs alongside the
fetch. SDK-first math (SECONDS_PER_YEAR, wholePercentToWAD, zeroFloorSub),
wadToBips dedupes the bips idiom, CAP_BUFFER_WAD lives in math, isIdle and
the foreign-IRM id list ride the snapshot. Startup checks extracted to
vault-checks.ts and the time gate to interval-gate.ts, both unit-tested;
strategy-config tables get shape validation; stale narration pruned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@haydenshively
haydenshively force-pushed the feat/vault-v2-reallocation-bot branch from 603ce33 to f96bc80 Compare August 18, 2026 06:13
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.

2 participants