Skip to content
CCPEDIAby Unity Nodes
Discussions/App Development/Correlated collateral and the parameter layer: notes for Canton vault buildersForum ↗

Correlated collateral and the parameter layer: notes for Canton vault builders

App Development1 posts38 views2 likesLast activity 3d ago
WO
woof-softwareOP
3d ago

Correlated collateral and the parameter layer: notes for Canton vault builders

Mykola Ilchuk, Head of R&D, Woof

Every lending market and every curated vault eventually answers the same question: what is a unit of collateral worth right now? For most assets the answer is a market feed. For correlated assets, liquid staking tokens, restaking tokens, yield-bearing vault shares, RWA yield tokens, a raw market feed is the wrong answer often enough to be dangerous, and the standard fix has a failure mode of its own. In March that failure mode cost Aave users about $26 million in wrong liquidations, with the oracle mechanism working exactly as configured.

Canton’s vault standard is being worked out across several teams right now. That makes this the right month to write down what we learned building this class of oracle for Compound, and what the March incident teaches about where the real risk lives.

Why correlated collateral needs a bound

Two obvious pricing approaches both fail for these assets.

A pure market feed inherits every liquidity problem the asset has. Correlated assets often trade thin, and a short depeg, a redemption queue backing up, a market-wide scramble for exit liquidity, prices perfectly solvent positions into liquidation. The holder’s claim on the underlying did not change. The order book did.

A pure fundamental read goes the other way. You take the exchange rate from the source contract, the stETH share rate, convertToAssets() on an ERC-4626 vault, and treat it as the price. Now a real loss, slashing, a frozen redemption window, an exploit in the underlying, never reaches your liquidation engine. And wherever the rate can be pushed from outside, donation-style attacks on vault share pricing are the classic case, the “fundamental” number is an attack surface.

The pattern that works in production prices from the exchange rate but bounds how fast that rate is allowed to grow. At initialization you snapshot the rate and its timestamp. From there, the maximum allowed rate follows a configured yearly growth ceiling, linearly approximated:

max_rate(t) = snapshot_rate * (1 + max_yearly_growth * elapsed_time / 1 year)

One property of this construction matters more than it looks. The approximation is linear while real yield compounds, and the ceiling has to stay tight enough to mean something, so the snapshot cannot live forever: it has to be refreshed on a cadence, or the cap drifts away from reality. Most designs treat that cadence as an operational afterthought. It is not. The periodic snapshot update is a permanent, automated write into the safety mechanism itself, and it is exactly where this pattern failed in production this year.

If the live rate exceeds the cap, the oracle returns the cap. The asymmetry is deliberate: growth faster than plausible yield means manipulation or error, and inflated collateral is where attacks make money, so the upside is bounded and rate drops pass through.

Be precise about what passes through, though. A rate read from the source contract only moves when the underlying claim moves: slashing, an exploit, a fee event. A market discount, the thing people usually mean by depeg, never appears in it at all. Whether a deployment prices at the capped rate alone or at the lower of market price and capped rate is not a detail; it decides who carries the risk of a redemption queue backing up. It is a per-asset policy choice, and an oracle interface does its job when it forces that choice to be made explicitly instead of burying it in an adapter default.

Two more honest limits. The cap is an envelope, not a validator: a value that climbs within the allowed growth budget passes, and after a long quiet stretch the gap between a stale snapshot and the cap is headroom an attacker can spend. And for vault shares, a cap blunts a donation-style spike but does not repair a vault whose share accounting can be pushed around; that fix belongs at the source.

This is CAPO, the Correlated-Assets Price Oracle. It came out of the Aave ecosystem, where BGD Labs first shipped it (bgd-labs/aave-capo), and by now it is standard practice for correlated collateral. We brought it to Compound and wrote that implementation, with adapters for wstETH, rETH, rsETH, generic ERC-4626 vaults, Chainlink-ratio assets and generic rate sources (github.com/woof-software/compound-capo).

The mechanism is also not where the story ends. A capped oracle introduces parameters: the snapshot pair, the growth ceiling, the update cadence. And parameters are where the next failure lives.

The $26 million configuration

On March 10, 2026, Aave’s CAPO deployment on the Ethereum Core and Prime instances started reporting a wstETH exchange rate of roughly 1.1939 while the actual rate stood near 1.228, about 2.85 percent low. Per The Block’s reporting and the Chaos Labs post-mortem, the cause was a mismatch between the snapshot ratio and the snapshot timestamp: the offchain process that produced the update did not account for an onchain constraint limiting how fast the ratio may step (3 percent per 3 days). The resulting configuration placed the cap below the live market rate.

From that moment the safety bound was the attack. The oracle undervalued collateral across the board, about 10,938 wstETH was liquidated across roughly 34 accounts, around $26 million in liquidation volume, and third-party liquidators netted roughly 499 ETH before the parameters were realigned. The DAO ended up compensating affected users from recovered funds and the treasury.

Read the incident from the mechanism’s point of view and the lesson is uncomfortable. No oracle was hacked. No feed lied. The formula did exactly what its configuration told it to do. Two values that are only meaningful together, the snapshot rate and its timestamp, were updated through a path where an offchain component and an onchain constraint disagreed. Nothing between proposal and execution checked the one invariant that mattered: the cap implied by the new configuration must not sit below the rate the market can currently observe.

Note also what the failure was not: nobody chose a bad growth number. The inputs were reasonable. The update path was unsound, and that distinction decides where the fix belongs.

A bound on the price is only as strong as the bounds on the process that moves it.

Why this multiplies in a curator model

What makes this instructive is not that a particular update path failed once. It is where the failure sat: in a single deployment, with a dedicated risk team, mature tooling and a well-understood asset, and it still came down to two values moving through a path that never checked them together. Now map that onto a curated vault standard, where the collateral choice sits with each curator rather than with the protocol.

In that model, cap configuration is no longer one team’s job. It is every curator’s job, per asset, forever. Most curators will not build snapshot-alignment checks, staleness monitors, or replay harnesses, and it is not reasonable to expect them to. If the standard leaves the bound and its parameters entirely per vault, the March failure mode ships once per curator.

There is also a Canton-specific reason to settle this now rather than later. The collateral this network is being built for is institutional: NAV-based instruments that update on a schedule, assets with redemption windows, tokenized funds whose market price is thin or gated by design. For that class, rate-plus-bound is not the exotic case, it is the default pricing model, and the per-asset semantics carry more weight than they do for an LST. A vault standard that gets the parameter layer right is not adding polish. It is deciding whether the collateral Canton actually wants can be priced safely at all.

This is why the interesting questions are interface questions, settled once in the standard rather than rediscovered per vault:

  1. Leave room for per-asset caps on top of the base feed. The oracle interface should let a curator tighten deviation and growth bounds for a specific collateral without forking the feed. If bounds exist only at feed level, every curator rebuilds them, and most will not. Tightening should also be the only direction the interface permits: a curator who can loosen a bound past what the feed guarantees has reintroduced the risk the bound existed to exclude.

  2. Make the snapshot atomic. The snapshot rate and its timestamp are one value with two fields. One setter, one transaction, or refuse the update. The March incident is precisely what the gap between them costs.

  3. Validate at execution time, not proposal time. A parameter change that was sane when proposed can be wrong by the time it executes, especially behind a delay. At execution, recheck the invariant on live state: the implied cap must not sit below the currently observed rate, and not implausibly above it. Halt instead of applying. The check is small enough to leave no excuse:

    // inside the same transaction that applies the update
    newCap = capAt(now, newSnapshot)
    require(newCap >= observedRate())
    require(newCap <= observedRate() * maxHeadroom)
    
  4. Decide where parameters live, not only who may change them. There are two families of answer here, and a standard is better off picking one deliberately than arriving at one by default.

    The first keeps parameters as mutable state and defends them. Changes move through a propose-then-execute window, and a liquidation already in flight references the pre-change value, so a position opened under one collateral factor is not seized mid-transaction under another.

    The second removes the mutable state. Compound v3 takes that route: market configuration is handed to the implementation’s constructor and held as immutable, including supplyKink, borrowKink, storeFrontPriceFactor, baseBorrowMin and targetReserves in CometWithExtendedAssetList.sol, with per-asset collateral factors and caps fixed the same way. Changing a parameter means deploying a new implementation and swapping the proxy in a single transaction through deployAndUpgradeTo. Between upgrades there is no race to lose, because there is no mutable parameter to read.

    Neither is free. Elimination costs a deployment per change, which is heavy if every curator tunes their own vault. Mitigation costs the discipline of getting the window right, permanently. The point is not that one is correct, it is that the choice is load-bearing and should be made in the open. It is worth noticing where March sits on this axis: the snapshot pair that drifted was mutable state written by an offchain process, which is the first family operating without the first family’s defenses.

  5. Replay before production. Parameters are code and deserve tests. Run a proposed configuration against recorded shock paths, March 2020, May 2021, November 2022, and now March 2026, and look at what it would have liquidated before it can liquidate anything real.

None of the five cares what executes the change. A multisig, a DAO vote, a threshold confirmation across independent operators: the shape of the governance layer is a separate decision, and a strong one improves the operational story considerably. What is not separable is where the validation runs. It has to live inside the same transaction that applies the parameter, because any gap between confirmed and applied recreates, in miniature, the exact window that March fell through.

What we would do differently with hindsight

None of this is exotic engineering. It is the operational layer that capped oracles tend to ship without, and March made the cost of that omission concrete.

Alarm on cap proximity. If the live rate sits within a small margin of the cap for more than a short window, one of two things is true: yield genuinely spiked, or the cap has decayed out of alignment. Both need eyes. In the March incident the live rate sat above the cap continuously; a monitor on how long the cap has been binding turns a day of wrong liquidations into minutes of paging.

Treat snapshot updates like deployments. Dry-run the update against current chain state, diff the implied cap before and after, and require the diff to be explainable by yield. A one-line check, cap after update versus observed rate, would have failed loudly in March.

Encode per-asset semantics in the adapter. wstETH’s ratio only grows. rETH’s can wobble with fee flows. An ERC-4626 share price can collapse in a block if the vault is exploited. One growth number does not describe all of these; the adapter should know what “impossible” means for its specific asset, and reject impossible readings rather than average over them.

The mechanism, in other words, was never the hard part. The hard part is the lifecycle around its parameters: who may change them, how the change is validated, what happens to positions in flight, and how a wrong value is caught before it liquidates someone. A capped oracle without that lifecycle is a $26 million incident waiting for a calendar date.

Canton’s vault standard has the rare advantage of being written after all of this happened. Settling the parameter layer at the interface costs a review cycle now. Leaving it per vault costs a repeat of March, multiplied by the number of curators.


Woof is an EVM engineering team and one of Compound’s core contributor teams. Our Compound implementation of CAPO is public at github.com/woof-software/compound-capo, and its design discussion is on the Compound forum. Criticism is welcome, especially from teams building vaults on Canton.

Incident figures per The Block’s reporting and the Chaos Labs post-mortem.

← Back to Discussions