Understanding Epochs
1. Primer: dependency chain
Cap credit is backed by restaker delegation held in a Symbiotic vault dedicated to your agent. Recovery on a bad loan means slashing that delegation. Three facts follow from how Symbiotic implements slashing, and together they determine everything else in this document.
Slashing is timestamp-addressed. A slash calls slashableStake(subnetwork, operator, captureTimestamp) and executes against the stake that existed at that timestamp, not against the current balance.
The capture timestamp is only valid for one Symbiotic vault epoch. Outside that range there is nothing to slash and the recovery path fails.
Therefore borrow capacity cannot be "what is delegated." It has to be what is slashable at a timestamp Cap can actually cite right now. That quantity is what Cap calls coverage, and it is the input to your LTV and health factor.
The Cap epoch exists to supply that timestamp. It is a protocol-wide checkpoint cadence that gives every agent a capture timestamp which is deterministic, derivable on-chain by any liquidator without off-chain input, and guaranteed to sit inside Symbiotic's window.
2. Two epoch clocks
Duration
7 days
3 days
Enforced by
Vault config; Cap rejects vaults below 7 days at registration
Delegation.epochDuration()
Determines
Width of the valid capture-timestamp window
Which checkpoint slashTimestamp falls back to
Phase
Vault creation time
Absolute Unix time — boundaries at timestamp % 259200 == 0
slashTimestamp is never more than two Cap epochs (6 days) old, against a 7-day Symbiotic window. The one-day margin is the reason Cap enforces a 7-day minimum vault epoch and refuses to register anything shorter.
3. slashTimestamp
slashTimestamp(agent) = max( (epoch() − 1) × epochDuration , lastBorrow − 1 )
The start of the previous Cap epoch is the floor. The timestamp is always inside the Symbiotic window, and is what a dormant agent is measured against. Its age oscillates between 3 and 6 days as the current epoch advances.
lastBorrow − 1 is the fast path. Cap writes lastBorrow = block.timestamp on every borrow. The instant before a borrow is by construction a moment when sufficient coverage existed, so it is both safe and far more recent. The − 1 is required because Symbiotic rejects captureTimestamp == now; Cap also decrements defensively if the computed value lands on the current block.
4. Calculating coverage
Delegation.coverage() does not read a single value. It takes the minimum of four measurements below to state what is recoverable.
1
slashableStake
Start of current Cap epoch
Delegation added mid-epoch, not yet checkpointed
2
slashableStake
slashTimestamp
Coverage that could not actually be slashed today
3
stakeAt (active)
Now
Stake already queued for withdrawal
4
slashableStake
now − 1
As #3, one block back
Quantities are snapshotted; prices are not. Each measurement snapshots the collateral amount at its timestamp, then values it at the current oracle price. Price movement therefore transmits to coverage with zero lag. The epoch mechanism lags quantity only.
Active and slashable stake diverge. A withdrawal request removes stake from active immediately while leaving it slashable until the withdrawal clears. Measurements 1, 2 and 4 read slashable stake; measurement 3 reads active stake. The min() means a withdrawal request cuts your capacity on request, not on settlement.
5. Refreshing slashTimestamp by borrowing
Every borrow writes lastBorrow. It is the only borrower-controlled input to the epoch machinery that can update the coverage amount of the restaker.
New delegation counts only once both lagging snapshots postdate it. They clear by different means:
The slash-timestamp snapshot you can move yourself. Any borrow at or above the reserve's
minBorrowsetslastBorrow = block.timestamp, pullingslashTimestampto one second ago. Effective in the same block, regardless of draw size.The epoch snapshot you cannot. It is pinned to the current epoch boundary and advances only when the clock crosses the next one.
Sequence after a restaker increases your delegation:
1
Wait for the next epoch boundary
Epoch snapshot now postdates the increase
2
Borrow ≥ minBorrow immediately after
slashTimestamp now postdates the increase
3
—
Full coverage live
Borrowing before the boundary accomplishes nothing — the epoch snapshot still predates the increase, and min() still selects it. Crossing the boundary without borrowing does work, but leaves slashTimestamp sitting on the previous-boundary floor, which costs another 3 days.
Constraints on the refresh
Borrows validate against pre-borrow coverage.
validateBorrowsizes againstmaxBorrowableas it currently stands, so the refresh draw cannot itself consume the capacity it unlocks. Refresh with a minimal draw, then size up.Repayment does not write
lastBorrow. Onlyborrowdoes.Over-refreshing is free. A
lastBorrowolder than the previous epoch boundary is simply ignored — the floor takes over — so there is no penalty for refreshing more often than strictly necessary.
6. Reference
Current epoch index
Delegation.epoch()
Capture timestamp in force
Delegation.slashTimestamp(agent)
Coverage driving your capacity
Delegation.coverage(agent)
Slashable collateral at that timestamp
Delegation.slashableCollateral(agent)
LTV / liquidation threshold (ray)
Delegation.ltv(agent) · Delegation.liquidationThreshold(agent)
Last updated