Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,26 @@ deviation OR 1 hour elapsed (whichever first). Volatile periods get pushes every
few minutes; the 1-hour heartbeat is the dead-market floor. Recommended
`maxAge = 2 hours` (one missed heartbeat tolerance).

> **⚠️ Required config invariant: `pauseTimeAfter >= maxAge`** (enforced at
> `initialize` — a violating config reverts `PauseTimeAfterBelowMaxAge`). The
> share price multiplies the DIA equity price by the vault's _live_ NAV ratio,
> and both must belong to the same corporate-action epoch. When an action
> completes the ratio rebalances instantly, but DIA can keep serving the
> pre-action price for up to `maxAge`. Only the post-window pause separates the
> two; if it lifts while a pre-action price is still fresh, that price pairs
> with the post-action ratio — on a 2:1 split the share reads ~2× its true
> value, enabling over-borrow (bad debt). Setting `pauseTimeAfter >= maxAge`
> guarantees only same-epoch prices are served. The exact boundary
> (`pauseTimeAfter == maxAge`) is airtight because the staleness check rejects
> the `maxAge` edge (`age >= maxAge` is stale), so at pause-lift the oldest
> still-fresh push is strictly newer than `effectiveTime`. A margin above
> `maxAge` is still recommended as defence-in-depth.
> **⚠️ Required config invariant: `pauseTimeAfter > maxAge` (STRICTLY)**
> (enforced at `initialize` — a violating config, including the equal boundary,
> reverts `PauseTimeAfterBelowMaxAge`). The share price multiplies the DIA
> equity price by the vault's _live_ NAV ratio, and both must belong to the same
> corporate-action epoch. When an action completes the ratio rebalances
> instantly, but DIA can keep serving the pre-action price for up to `maxAge`.
> Only the post-window pause separates the two; if it lifts while a pre-action
> price is still fresh, that price pairs with the post-action ratio — on a 2:1
> split the share reads ~2× its true value, enabling over-borrow (bad debt).
>
> The margin `pauseTimeAfter - maxAge` is the maximum forward DIA feed clock
> skew the config tolerates. Staleness is aged from the push's own _source_
> timestamp, so a feed running `skew` seconds fast can stamp a pre-action
> observation up to `skew` after `effectiveTime`; only a margin exceeding that
> skew guarantees every still-acceptable push at pause-lift was observed at or
> after the action. Equality (`pauseTimeAfter == maxAge`) tolerates **zero**
> skew — a feed even one second fast reopens the 2× window — so it is rejected.
> Size the margin above your feed's worst-case forward skew; the recommended
> `maxAge = 2 hours` with `pauseTimeAfter = 3 hours` (a 1h margin) is the
> reference.

**DIA on Base mainnet:** the canonical oracle contract is
`0xCE521b52513242c5094bc56f57887BB2A05B8129`. Per-symbol feeds are all served
Expand All @@ -91,7 +97,7 @@ DIAVaultOracle oracle = diaVaultOracleBeaconSetDeployer.newDIAVaultOracle(
maxAge: 2 hours,
actionTypeMask: type(uint256).max,
pauseTimeBefore: 1 hours,
pauseTimeAfter: 3 hours // >= maxAge, with a margin (see invariant above)
pauseTimeAfter: 3 hours // > maxAge, with a skew margin (see invariant above)
})
);
```
Expand Down
6 changes: 4 additions & 2 deletions src/concrete/deploy/MorphoPairAdapterBeaconSetDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ contract MorphoPairAdapterBeaconSetDeployer {
/// The beacon for the MorphoPairAdapter implementation contracts.
IBeacon public immutable iMorphoPairAdapterBeacon;

/// The central multi-pair price store every adapter deployed through this
/// beacon reads — fixed for the beacon's whole life.
/// The central multi-pair price store recorded at deploy time. This is the
/// central baked into the FIRST implementation; a beacon upgrade can retarget
/// live adapters to a different central while this value is unchanged — see
/// the config-struct NatSpec for why it is not a live invariant.
ST0xPriceOracle public immutable iCentral;

constructor(MorphoPairAdapterBeaconSetDeployerConfig memory config) {
Expand Down
4 changes: 2 additions & 2 deletions src/concrete/oracle/DIAVaultOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ error HistoricalRoundDataUnsupported(uint80 roundId);
/// @param maxAge Maximum acceptable DIA push age in seconds.
/// `block.timestamp - timestamp >= maxAge` reverts `DIAPriceStale` (the edge
/// instant fails closed — a push exactly `maxAge` old is stale). Immutable
/// after init — redeploy a fresh proxy to change. MUST be `<= pauseTimeAfter`
/// (see `pauseTimeAfter`).
/// after init — redeploy a fresh proxy to change. MUST be `< pauseTimeAfter`
/// STRICTLY (a positive margin is required — see `pauseTimeAfter`).
/// @param actionTypeMask Bitmap of action types that trigger the auto-pause.
/// `ACTION_TYPE_STOCK_SPLIT_V1` for splits only, or `type(uint256).max` for
/// every present and future action type. Must be non-zero.
Expand Down
7 changes: 4 additions & 3 deletions test/src/concrete/oracle/DIAVaultOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ contract DIAVaultOracleTest is Test {
/// which condition dominates.
///
/// The overlap is not contrived: it is the normal state late in a post-action
/// window, since `pauseTimeAfter >= maxAge` guarantees every push predating
/// window, since `pauseTimeAfter > maxAge` guarantees every push predating
/// the action has gone stale before the pause lifts. Here the push sits
/// exactly on the staleness edge (`age == maxAge`) while the completed action
/// is still mid-window.
Expand Down Expand Up @@ -672,8 +672,9 @@ contract DIAVaultOracleTest is Test {

/// @notice The staleness edge fails closed: a push aged EXACTLY `maxAge`
/// reverts `DIAPriceStale` (`age >= maxAge` is stale). This edge-rejection
/// is what makes the cross-epoch invariant airtight at `pauseTimeAfter ==
/// maxAge` — see the contract NatSpec.
/// tightens the cross-epoch invariant by one second; the invariant itself is
/// closed by the strict `pauseTimeAfter > maxAge` init margin — see the
/// contract NatSpec.
function testLatestAnswerAtMaxAgeBoundaryIsStale() external {
DIAVaultOracle oracle = _deployProxy(_defaultConfig());
uint128 boundary = uint128(block.timestamp - MAX_AGE);
Expand Down
Loading