Skip to main content
Guardians who Approve are underwriters. ExposureLedger is the coverage book: it prices what a proposal can extract, books the WOOD a guardian locks against that price when it approves, and at execute tells the governor how much coverage actually stands behind the calldata. A coverage shortfall does not disqualify the proposal. It scales how much capital may leave the vault. Guardian daemons that treat a shortfall as a hard fail are implementing a rule the contracts do not enforce. The proposer bond is a separate WOOD pull at propose, quoted from this same requiredCoverage.
ExposureLedger is part of the launch’s second deployment run. Until a governor has a ledger wired, it skips the covered-TVL cap, the horizon, the bond and the execute-time quorum, and runs every proposal at its declared maxCapital. Addresses: Deployments.

requiredCoverage

SyndicateGovernor._snapshotTierAndGate writes proposal.requiredCoverage at propose. Read it later with getRequiredCoverage(proposalId). Coverage is the sum of per-call contributions across both execute and settlement calls:
boundBps_i comes from TierRegistry.tierOf(target, selector):
  • Tier 0 / 1: the certified extractable bound for that (target, selector) pair.
  • Tier 2 or uncertified: 10_000, the full notional of the declared cap.
The proposal’s tier is the max tier across execute and settlement calls. An uncertified strategy therefore books roughly twice its deployed capital: the execute leg and the settle leg each price at full notional. The Portfolio class is uncertified at launch; its certification at 2,000 bps is pending. See Tier policy. A tier-2 call carries a second, independent propose-time limit: its declared cap_i must fit inside totalAssets() × tier2CallCapBps / 10_000, or propose reverts Tier2CallCapExceedsCeiling(i).
Zero coverage is specified, not a hole. An all-zero-cap batch prices to zero regardless of tier. requiredCoverage == 0 skips the approve-quorum gate and needs no bond; a proposal that can extract nothing has nothing to underwrite. The per-call meter is the protection: cap_i == 0 makes BatchExecutorLib revert CallCapExceeded on any outflow.

Propose-time gates (these do block)

These run in _snapshotTierAndGate against the declared coverage, before anyone underwrites: coveredTvlCapUsd == 0 is fail-closed: nothing with non-zero coverage is proposable until the owner seeds the cap. At execute, _resolveTierAndCoverage runs again. If the live tier is higher than the snapshot, executeProposal reverts TierRegressed; if live coverage is higher, CoverageRegressed. Those are regression guards (a demotion between propose and execute), not the shortfall path.

Approve is underwriting

The hook is GuardianRegistry.voteOnProposal(governor, proposalId, support, lockWood) → ExposureLedger.recordApproval(governor, proposalId, guardian, lockWood). On a first Approve, and on a Block → Approve change, the registry writes its own vote state then calls the ledger. The ledger call is not wrapped: if it reverts, the vote reverts, so an approver slot is never seated without a lock behind it. The guardian declares the size of its own bet. lockWood is the fourth argument of the vote, the WOOD the approver puts behind this proposal. It is ignored on a Block; pass 0. The ledger books:
With kNumerator = 1, a guardian’s locks never sum to more than its stake, so one conviction cannot leave its other proposals under-covered. Over-declaring clamps; under-declaring is refused. A lock larger than the free budget books less, and the difference surfaces at execute as a shortfall. A lock that is too small reverts ApproveLockBelowFloor:
  • shareUsd = ceil(needUsd / 100), one approver’s share if a hundred carried the book (MAX_APPROVERS_PER_PROPOSAL is 100; blockers are uncapped).
  • budgetUsd is the guardian’s whole cap, min(cap, stake) × woodPriceX8 / 1e8.
  • floorUsd = shareUsd if shareUsd < budgetUsd, or if 50 or more approvers are already booked; otherwise floorUsd = budgetUsd. A guardian whose whole budget is below one share is admitted with its whole budget only while fewer than half the slots are taken.
  • Reverts if lock == 0, budgetUsd == 0, or the booked lock is worth less than floorUsd.
Every WOOD amount is valued at the haircut woodPriceX8(), so a starved or inflated feed can refuse a slot but never enlarge one. An Approve also needs the ledger’s inputs: it reverts CoverageInputsUnreadable, StalePrice / FeedNotConfigured (the vault-asset feed), NoWoodPrice (the WOOD feed) or CoverageHorizonExceeded. A Block reads no price, so it keeps working through a pricing outage.

Lock lifetime

A lock sits in the epoch bucket that holds executeBy + strategyDuration and is held until bucketEnd + challengeWindow, whether or not the proposal executed. At the launch constants (epochLength 7 days, challengeWindow 14 days) a lock lives about 23–53 days for a 7- to 30-day strategy, and it keeps counting against the guardian’s free budget and blocks claimUnstakeGuardian (CoverageStillOpen) until then.
  • releaseApproval is the unwind on Approve → Block, registry-only; CoverageFrozen while a challenge is live.
  • retireApproval is permissionless once the bucket has aged out; before that it reverts ChallengeWindowOpen, and CoveragePinnedActive while an inconclusive challenge round pins the lock.
  • openExposure(guardian) walks the buckets in WOOD, with no price, across a 60-day horizon.
The execute-time quorum reads the ledger’s own approver list, never the registry, so the two cannot disagree about who covered a proposal.

Shortfall scales rather than blocks

This is the execute-time rule. Guardian daemons that treat a coverage shortfall as disqualifying are wrong. IExposureLedger.requireApproveQuorum is a coverage measurement with a zero floor, not an all-or-nothing gate. It returns (coverageRaisedUsd, requiredCoverageUsd) so the caller can size execution to a coverage-proportional effective capital. It reverts only when the approver set is empty or the raised aggregate is exactly zero (InsufficientApproveCoverage). A nonzero-but-partial aggregate is reported to the caller. SyndicateGovernor._deriveAndStoreEffectiveCapital is that caller. It runs inside executeProposal after executedAt is stamped, in the same transaction. The gate runs when:
There is no tier threshold: every tier is gated. When the gate does not run, effectiveMaxCapital stays maxCapital. When it does:
That is floor integer division. Dust coverage can floor effectiveMaxCapital to zero, a zero net-outflow cap, fail-closed. The division never sees a zero denominator on the scaling branch: requireApproveQuorum already reverted on a raised aggregate of exactly zero. The same ratio scales every per-call cap via _scaleCaps ((caps[i] * raised) / required). Scaled settlement caps are persisted so settleProposal reuses them verbatim and never recomputes coverage. A coverage drop between execute and settle cannot cap the unwind below the size legitimately deployed. executeProposal emits EffectiveMaxCapitalSet(proposalId, declaredMaxCapital, effectiveMaxCapital, coverageRaisedUsd, requiredCoverageUsd).
Silence still does not pass a gated proposal. Empty approvers or a raised aggregate of exactly zero does revert InsufficientApproveCoverage. The proposal stays Approved and expires at executeBy unless a covering Approve arrives. That is “no underwriter on the hook,” not a shortfall. A partial book is the shortfall case, and it scales.

What requireApproveQuorum measures

requiredCoverageUsd = coverageUsd(asset, requiredCoverage), the vault asset’s Chainlink feed to USD (18 decimals), stale past 26 hours. Each approver contributes min(lock, live slashable stake) × woodPriceX8() / 1e8: the lock it declared, valued at the haircut price now, and no more than its stake is worth now. A guardian whose lock is worth less than when it declared it (an unstake request, a WOOD price fall) counts at the shrunken live value, so coverage must hold in dollars at execution. The loop sums locks; there is no pro-rating and no cross-proposal sharing. This is not an indemnity. coverageRaisedUsd is bonded conviction standing behind the proposal. Slash proceeds are burned, not paid to anyone harmed. requiredCoverage is the price of admission to a tier, expressed in the same dollars a loss would be because that is the natural scale, not because the two net out.

WOOD pricing

woodPriceX8() is min(feed, woodUsdPriceX8 cap) × woodHaircutBps / 10_000, floored at 1, and reverts NoWoodPrice when the cap is zero or the feed cannot answer. NoWoodPrice halts every Approve, every propose and execute with non-zero coverage, challenge filing and fee weights, while Block votes, settlement and the queue keep working.

Views

See Guardian Review for the Approve / Block vote itself, and Execution & Settlement for the post-approval path that consumes effectiveMaxCapital.