Skip to main content

SpecificationsImpairment state machine

Impairment state machine

The states, roles, transitions, and errors behind CreatorOVault impairment, for integrators, auditors, and contract developers.

This is the onchain side-pocket for a failed strategy in CreatorOVault. It is written for developers and auditors; holders should start with Impairment v1 disclosures.

Where it runs

The facade ABI is on CreatorOVault; transitions run in CreatorOVaultCoreModule via delegatecall. This machine ships with impairment v1 for new vault launches, and stays in Normal unless a strategy is tripped.

The lifecycle in words​

  1. A guardian or the owner trips impairment on a strategy. The vault moves to Suspect and freezes deposits and withdrawals.
  2. Management proposes a Merkle root of who gets claims, which opens a challenge window.
  3. Anyone may challenge the root during that window; management rejects the challenge or clears the root.
  4. After the window, management finalizes. Claims can then be minted, keepers notify recoveries, and holders claim them.
  5. Once recovery accounting closes, the epoch is resolved and the vault returns toward Normal.

A false alarm, or a trip left too long without a root, can be cleared back to Normal instead.

States​

The vault has a mode, and each impairment epoch has its own status.

Vault mode​

ModeMeaning
NormalClean book. ERC-4626 ingress/egress live (subject to pause/shutdown/whitelist).
SuspectActive tripped epoch. deposit / mint / withdraw / redeem revert. maxDeposit / maxMint / maxWithdraw / maxRedeem return 0.

Epoch status (ImpairmentEpoch.status)​

StatusMeaning
NoneUnused id.
TrippedGuardian/owner froze a strategy. Optional Merkle root + challenge window.
FinalizedRoot locked. Claims can mint. Recovery can notify/claim.
ResolvedRecovery accounting closed. Root remains claimable (false-alarm clear does not).

Defaults​

Constructor defaults: Normal, impairmentChallengeWindow = 1 days, maxImpairmentTripDuration = 14 days (bounds 3–30 days), maxImpairmentChallengesPerEpoch = 3. impairmentChallengeBond = 0 disables the ETH bond.

Who may call​

Each transition is restricted to one role, except the liveness and claim paths, which anyone can call.

ActorTransitions
owner ∪ impairmentGuardiantripImpairment, clearImpairmentTrip
managementproposeImpairmentRoot, rejectImpairmentChallenge, clearImpairmentRootAfterChallenge, finalizeImpairment, reinstateImpairedStrategy
keepernotifyImpairmentRecovery
AnyoneclearStaleImpairmentTrip, challengeImpairmentRoot, mintImpairmentClaim, claimImpairmentRecovery

Config (setImpairment*) is admin/owner on the facade → admin module. Not part of the epoch graph.

Transitions​

The full graph, with the guard and revert reasons for each call.

Normal
│ tripImpairment(strategy, reasonCode)
│ onlyImpairmentAuthorized
│ revert: VaultNotNormal, ImpairmentAlreadyActive,
│ StrategyAlreadyImpaired, ImpairmentRetripCooldown,
│ InvalidImpairmentReason
▼
Suspect + epoch.Tripped
│
├─ clearImpairmentTrip(epochId) authorized
│ zeroes root / claim supply / recovery asset
│ starts re-trip cooldown = maxImpairmentTripDuration
│ → Normal
│
├─ clearStaleImpairmentTrip(epochId) permissionless
│ after trippedAt + maxImpairmentTripDuration
│ revert if snapshotRoot != 0 (use challenge / clear-root)
│ also starts re-trip cooldown
│ → Normal
│
└─ proposeImpairmentRoot(epochId, root, totalClaimSupply, recoveryAsset)
onlyManagement
challenge unlock must be < stale deadline
│
├─ challengeImpairmentRoot(epochId, reason) payable, anyone
│ during challenge window; bond + per-epoch cap
│ ├─ rejectImpairmentChallenge management (slash / keep bond)
│ └─ clearImpairmentRootAfterChallenge management → root cleared
│ then re-propose or clear trip
│
└─ finalizeImpairment(epochId) management
after window, no live challenge
▼
epoch.Finalized
├─ mintImpairmentClaim(epochId, account, amount, proof)
├─ notifyImpairmentRecovery(epochId, amount) keepers
└─ claimImpairmentRecovery(epochId, receiver, units)
→ epoch.Resolved (vault returns toward Normal)
reinstateImpairedStrategy once book is clean

Liveness guards​

  • #157050: a false-alarm or stale clear starts a re-trip cooldown of maxImpairmentTripDuration so the authority cannot clear+re-trip to refresh the stale deadline forever.
  • ODA-497-2: clearStaleImpairmentTrip is refused while snapshotRoot != 0.

Errors (transition-critical)​

Custom errors the transitions can revert with.

VaultNotNormal, VaultNotSuspect, NoActiveImpairment, ImpairmentAlreadyActive, InvalidImpairmentEpoch, InvalidImpairmentTransition, StrategyAlreadyImpaired, StrategyNotImpaired, InvalidImpairmentReason, ImpairmentRootNotReady, ImpairmentChallengeWindowClosed, ImpairmentRootRequired, ImpairmentRootAlreadyFinalized, ImpairmentRootWouldExceedStaleDeadline, ImpairmentRootBlocksStaleClear, ImpairmentRootChallengedErr, ChallengeWindowNotConfigured, ClaimAlreadyMinted, InvalidClaimProof, NothingToClaim, RecoveryEscrowNotConfigured, ClaimSupplyExceeded, ClaimSupplyExceedsTripShares, InvalidImpairmentTripDuration, InvalidImpairmentChallengeWindow, ImpairmentTripNotStale, ImpairmentRetripCooldown, ImpairmentChallengeCapExceeded, ImpairmentRootAlreadyChallenged, InsufficientChallengeBond, ImpairmentChallengeBondTransferFailed, NoActiveImpairmentChallenge, InvalidMaxImpairmentChallenges.

Events​

Events to index for tracking an epoch.

ImpairmentTripped, ImpairmentTripCleared, ImpairmentTripClearedByTimeout, ImpairmentRootProposed, ImpairmentRootChallenged, ImpairmentRootCleared, ImpairmentRootFinalized, ImpairmentFinalized, ImpairmentRecoveryNotified, ImpairmentRecoveryClaimed, ImpairmentResolved, ImpairmentChallengeBondSlashed, ImpairmentChallengeBondRefunded, ImpairmentChallengeBondRefundFailed, plus config updates (ImpairmentChallengeWindowUpdated, …).

Test checklist​

Existing coverage lives in test/CreatorOVault.ImpairmentV1.t.sol. Any change to this machine should keep these properties green:

  1. Happy path. trip → propose root → wait window → finalize → mint claim → notify recovery → claim → resolved. Vault returns to Normal. Impaired strategy excluded from totalAssets until reinstated.
  2. False alarm. clearImpairmentTrip returns to Normal, zeroes root/claim surface (ImpairmentRootRequired after clear). Re-trip before cooldown reverts ImpairmentRetripCooldown.
  3. Stale liveness. After maxImpairmentTripDuration, anyone can clearStaleImpairmentTrip if snapshotRoot == 0. While a root is proposed, stale-clear reverts (ImpairmentRootBlocksStaleClear).
  4. Challenge game. Bond too low / window closed / cap exceeded / already challenged revert. Management can reject or clear-root after challenge. Finalize blocked while challenged.
  5. Finalization gates. Cannot finalize before unlock. Cannot mint before finalize. totalClaimSupply is a hard mint cap (ClaimSupplyExceeded).
  6. Recovery isolation. Escrow rejects claims past recovered-for-that-epoch. Creator-coin recovery uses tracked-balance so clean-book totalAssets does not double-count.
  7. Suspect freeze. While Suspect, max* are 0 and sync ERC-4626 in/out revert.
  8. Auth. Non-guardian cannot trip. Non-management cannot propose/finalize. clearStaleImpairmentTrip stays permissionless.

Do not add product methods for new impairment features on CreatorOVault — extend the core module and keep the facade as a _delegate ABI.