kUSD / USDC market on Euler (Base): a redeemability-verifiable synthetic dollar

Summary

We are Kerne. We are building kUSD, a delta-neutral synthetic dollar on Base, and we would like to open an isolated Euler v2 market with kUSD as collateral and USDC borrowable. We are posting here because Euler is the one lending surface designed to onboard new stablecoins and their yield-bearing derivatives in isolated, risk-contained markets, and because we think kUSD brings something specific to that surface: a dollar whose backing you can verify yourself, on-chain, right now.

We are early and we will say so plainly throughout this post.

What kUSD is

kUSD is a synthetic dollar backed by a delta-neutral basis position: spot ETH collateral on Base, hedged with a short ETH perpetual on Hyperliquid, plus an on-chain Peg Stability Module. kUSD mints and redeems against USDC through that PSM, so the peg is enforced by redeemability, not by secondary-market depth.

  • kUSD v2 (Base): 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 (verified on BaseScan and Sourcify)
  • skUSD, the ERC-4626 staked wrapper: 0xdEd74F7E06efc76455C07418b8b74Cc2bc009DB4
  • USDC (Base): 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
  • Governance: 2-of-3 Gnosis Safe 0x52d3E450bA6c299B1B07298F1E87DD74732D4877; core vault logic is immutable and the deployer key is renounced.

Note on the ticker: kUSD here is the Kerne Synthetic Dollar at the Base address above. Please match on that address, not the symbol string, since “kUSD” is used by other projects on other chains.

Honest stage

We are genuinely at genesis. Outstanding kUSD supply is on the order of a hundred tokens and total collateral is in the low hundreds of dollars. We have not completed an external audit yet (an audit engagement is in progress). We are not going to dress that up. We are asking for a small, conservative market, not a large one.

Why we think kUSD is a good fit for an isolated Euler market

  1. The backing is verifiable. We publish an hourly EIP-191 signed proof-of-reserve at kerne.fi/api/por/signed that binds the PSM USDC reserve and the outstanding kUSD liability to a signature you can recover yourself. Anyone can verify it in-browser with no key at Verify any signed attestation, in your browser | Kerne. Most stablecoins ask you to trust an attestation; here you can re-derive the solvency ratio from public inputs.
  2. There is a live precedent to underwrite against. We already run a permissionless Morpho Blue market on Base with kUSD as collateral and USDC as the loan asset, at a 62.5% liquidation LTV (market id 0xdc6a28b2941b3d32affc71cdeeb10e9779ff2a8ba1dcabc258e7b61f9a11beb3). An Euler market would give kUSD a second, looping venue and, on Frontier, a distribution surface it does not have today.
  3. It puts anchor capital to work. Right now kUSD can only sit in a self-seeded Aerodrome pool. As Euler collateral it can be borrowed against and looped, which is the difference between a deposit sitting flat and the same deposit generating real borrow-side activity.

Oracle

This is the part we care most about getting right, because kUSD is thinly traded and a naive market-price oracle would be a bad-debt vector.

kUSD’s only DEX venue is an Aerodrome stable pool of roughly 240 USD depth. A price read off that pool is trivially manipulable, so we do not use it as a price input. We propose one of two oracle configurations:

  • Baseline: a FixedRateOracle(kUSD, USDC, 1_000_000) (1 kUSD = 1.000000 USDC) chained to USD through the Chainlink USDC/USD adapter. This matches how Frontier prices other pegged stablecoins. It is depeg-blind by design and bounded by the conservative LTV below.
  • Preferred: a custom IPriceOracle adapter we fund and deploy that prices kUSD from the on-chain PSM reserve over outstanding-liability ratio, capped at the peg, using the thin pool only as a fail-closed tripwire. It marks kUSD down in real time if the backing is ever impaired, while staying unmovable by a few hundred dollars of pool flow.

For skUSD as collateral, the EulerRouter can price it natively with govSetResolvedVault(skUSD, true), which resolves shares via convertToAssets on top of the kUSD/USD adapter. We have written the full feed spec and can share it.

Proposed risk parameters

Conservative, sized to a genesis asset. We would rather open small and raise on clean utilization.

Parameter Proposed
Collateral kUSD (and optionally skUSD)
Borrowable USDC
Borrow LTV 60%
Liquidation LTV 65%
Supply cap (launch) 50,000 kUSD, raise to 250,000 on clean utilization
Borrow cap (launch) 40,000 USDC, raise to 200,000 with the supply cap
IRM adaptive / kink, target utilization 90%, base rate 0%, approximately 5 to 8% APY at kink

The 65% liquidation LTV caps a recursive loop at about 2.86x and matches the LTV we already run on the live Morpho market, so there is a public data series to underwrite against. We are happy to defer final caps and curve constants to Euler Labs or a risk reviewer.

What we are asking for

Guidance on the cleanest path to a kUSD market on Euler v2 (Base): whether to self-deploy an ungoverned EVK vault and request Frontier inclusion, or to run this as a managed market with a risk reviewer setting parameters. We are ready to deploy the oracle adapter and the vaults, and to iterate on parameters.

Links:

Thanks for reading. Happy to answer anything, including the parts that are still early.

Quick update to keep this current. Three things have moved since we posted.

First, the book: outstanding kUSD is now about 1,114.74 (it was on the order of a hundred at posting), backed by a combined 1,115.85 USDC across the two PSM contracts, roughly 100.10% on the PSM leg. This is still founder-seeded genesis capital and concentrated, we are not implying organic demand, but flagging the corrected figure so any review is against live numbers.

Second, two housekeeping corrections to the original post. On addresses: the staked-vault (skUSD) address predates a July 3 redeploy that reset the vault share-price accounting to par, so the live staked vault is now 0x96F5102C15b839757f811A98CEc3725Ac21DfA14 (kUSD itself is unchanged at 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3, and the old staked-vault address is retired). On governance: the original post said the deployer key is renounced, which is not yet accurate for the redeployed staking vault; it is currently deployer-controlled with rotation to the 2-of-3 Safe pending, and the live custody state is always at skUSD Admin Status | Kerne Protocol.

Third, a security audit is underway, with fieldwork started this week, which was the main open gap when we posted. kUSD’s protocol page is also live on DefiLlama now, and their maintainers merged our price feed fix this month, so the TVL can be read off-site rather than only on our endpoints.

Everything remains verifiable at kerne.fi/api/por/signed. Still asking for the same small, conservative market, and happy to hand a risk reviewer the full parameter sheet and the oracle feed spec whenever useful.

On the Morpho: the proposal describes “a live precedent to underwrite against” and “a public data series,” but the linked market currently shows $0 supplied, $0 borrowed, no transaction history, and carries Morpho’s "Market is not listed’ warning, with a fixed 1:1 oracle. Can you clarify what data series a reviewer should underwrite against, or should this argument be set aside until the market has activity?

You are right to press on this, and the short answer is yes, set the data-series argument aside. Let me be precise about what the Morpho link supports.

The market is real and on-chain: it exists at the id in the post, kUSD collateral against USDC, roughly 62.5% liquidation LTV, 1:1 oracle. It has no usage yet. Supply and borrow are zero and there are no liquidations to study. The “Market is not listed” banner is a separate matter: it means the market was created permissionlessly and is not curated or whitelisted, which is true regardless of activity. So “a public data series to underwrite against” was the wrong claim and I withdraw it. There is no history of borrows, repayments, or liquidations to fit anything to until the market has flow. Thanks for catching it.

What that deployment shows is narrower. We committed the same conservative liquidation LTV we are proposing here to a live, immutable market, and that parameter is fixed on-chain and cannot be edited. It speaks to the parameters only. It says nothing about demand, and I should have scoped it that way in the post.

On the real question, what a reviewer can evaluate for a market this early, there are two things that do not depend on a utilization history.

First, the backing, read directly on-chain. kUSD mints and redeems 1:1 against USDC through the PSM, so the peg is a redeemability property, not a liquidity one. The live mint module holds the kUSD MINTER_ROLE and the retired instances have it revoked, which you can read in one call each. The USDC reserve behind outstanding kUSD is published hourly as an EIP-191 signed statement at kerne.fi/api/por/signed and is re-derivable in-browser with no key. For a market where kUSD is the collateral, that solvency read is what you underwrite, and it holds whether or not the Morpho market ever sees a deposit. Right now it is about 1,115.85 USDC of reserve against about 1,114.74 kUSD outstanding, roughly 100.1 percent.

Second, the stage. That outstanding kUSD, the ~1,114.74 above, is founder-seeded and concentrated, with no organic demand yet. I will not dress that up as traction. It is why we asked for a small market sized to genesis and offered to defer final caps and the curve to a risk reviewer.

One process update relevant to a reviewer, since this thread went quiet: our first external review, by Hexens, delivered its initial report on July 20 and remediation is underway. I will not call the code audited until the final report is public, but the engagement is well past the fieldwork stage.

To put a concrete ask on it: underwrite the genesis market on the verifiable backing and the conservative parameters, since there is no demand history to use. If it helps, I can hand a reviewer the full oracle feed spec and the parameter sheet so caps get sized against the reserve, which is the number we can actually verify.

Thank you, that’s a clear rescoping.

One follow-up on the verifiable backing. The PoR and the preferred oracle both measure one thing: USDC in the PSM against outstanding kUSD. That works today because kUSD is 100% PSM-backed. But the product’s actual backing is ETH plus a Hyperliquid short that sits outside the PSM. The endpoint already shows the start of it: an offchain block with self-reported Hyperliquid equity, a WARNING_DELTA status, and two different solvency ratios(1.0011 PSM-only, 1.0244 including off-chain assets). As the basis position grows, the solvency number comes to rest on exchange data nobody can verify on-chain. The exact trust assumption the signed PoR is meant to remove. What is the plan, and which of the two ratios should a reviewer treat as the solvency read?

Both halves of that are fair. Let me take the direct question first.

Which ratio to underwrite against: the PSM one, 1.0011.

That is the solvency read. It is also what the signed statement already leads with: aggregate_solvency_ratio carries the PSM basis since schema v4, with aggregate_solvency_ratio_basis stating so in the payload. The field name is a leftover from the pre-v4 schema and I can see how it reads confusingly next to aggregate_solvency_usd_ratio, which is the 1.0244 you quoted (1.0248 as I write this).

The reason the PSM ratio is canonical is not that it flatters us. It is barely above par and it will always be the least impressive number we publish. It is that every input is an on-chain quantity you can recompute without us: USDC.balanceOf on the three PSM contracts, over kUSD.totalSupply() minus the kUSD those same contracts hold. Seven read calls on Base, no signature required, nothing sourced from a venue. Right now that is 1,113.885006 USDC against 1,112.707154 kUSD outstanding.

The USD aggregate is disclosure, not an underwriting metric, for two reasons. It includes the off-chain leg, which is the part you correctly say cannot be independently verified in full. And it is not stable across our own two surfaces: as I write this the signed statement says 1.0248 and the unsigned /api/por says 1.0354. Both are honest and the gap is explained in the endpoint text (the signed statement attests vault v2, whose only asset is the on-chain mirror of the Hyperliquid margin, while /api/por reads the retired v1 vault, where roughly 11.56 USD of residual WETH also sits). That is a small spread on a book of about 1,113 USDC, but a number whose value depends on which of our own surfaces you read is not one anybody should underwrite against. The PSM ratio does not have that property.

On WARNING_DELTA.

It is the hedge-delta field, not a solvency field. In that same statement is_solvent and psm_solvent are both true. It fires because the delta check measures the Hyperliquid short against ETH-price-exposed collateral inside the attested vault, and that is zero: v2 holds no user deposits and no spot ETH, and you can read it in the signed payload itself as chains[0].eth_exposed_eth. A short with no exposed spot behind it in the attested base reads as a fully directional book, so the status resolves to the warning. The measurement is correct about what it measures. The ETH the short is actually sized against is a founder-custodied watch-only float, published at /api/por under reserves.protocolOwnedReserves.components.founderWatchOnlyFloat, which backs no kUSD and enters no ratio.

We could clear that flag tomorrow by widening the hedge base to include that wallet. We are not going to. A solvency surface that counts a founder wallet as backing is worse than one that carries a visible warning, so the flag stays until the hedge runs against collateral that actually backs kUSD. I would rather the endpoint flag itself than smooth it.

On the structural point, which is the real one.

You are right. The PSM ratio is sufficient today only because kUSD is 100% PSM-backed and the basis book is a disclosed pilot sitting outside the backing. If the basis position grows into the backing, the PSM-only ratio stops being sufficient, and I am not going to argue otherwise.

So the rule, stated publicly so it can be held against us: off-chain assets never enter the canonical solvency read. If an ETH-plus-hedge leg ever backs kUSD, it enters the canonical ratio only once it is independently attestable, and until then it is carried in its own field with its own status and never merged into the headline. If we ever quietly move the headline onto a number that includes a venue balance, that is the thing to call us on.

On “exchange data nobody can verify on-chain”, it splits into two parts, and only one of them is a real gap.

The balance is not a self-report. Hyperliquid publishes per-address state, the venue account is named in the endpoint, and the exact request is published beside it:

POST https://api.hyperliquid.xyz/info with {"type":"clearinghouseState","user":"0x09a2780ac8Be6D5d2d1F85A8D92b09D40C9CA37e"}

Right now that returns accountValue 26.637393 and a single ETH position of -0.0079. You can run it without us, and /api/por recognises the lower of that live read and the on-chain l1Assets mirror, so the mirror can never overstate the venue.

There is also an attribution fact we have not stated clearly enough: the key that signs the hourly attestation is the same address as the Hyperliquid account it reports. Recover the signer from /api/por/signed and compare it to reserves.vault.composition.l1Assets.venue in /api/por. “This account is ours” is therefore a signature from the account itself rather than a claim.

That equality cuts both ways and I would rather name it than have you find it. It also means the attestation signing key is a live venue key. Separating the two is easy on its own, but it costs the attribution property unless we publish a signed link between them, which is part of what the enumeration below has to solve.

What is genuinely not covered is completeness. Nothing above proves that account is the only one, or that there is no offsetting position somewhere else. A per-address read cannot prove a negative about addresses nobody asked it about, and that is the trust assumption still standing. Closing it is the roadmap: a signed enumeration of every venue account on the same hourly cadence, so “these are all of them” is itself a signed claim with a signer attached, and the per-account reads pulled into the same statement so the PoR signature binds them. Plus a constraint on ourselves, that we only hedge on venues with public per-address state, so no leg is ever unreproducible by a third party.

The machine-readable version of the current state is already up if it is useful. /api/por carries a hedgeScope field at reserves.protocolOwnedReserves.components.founderWatchOnlyFloat.hedgeScope, naming which balances are inside the hedged scope and which are deliberately left unhedged and why, and kerne.fi/facts.json carries the same as a structured hedge object.

One question back, since you are the person whose answer actually matters here. What would that enumeration have to look like to be worth anything to a reviewer? A signed message per account, a third-party attestor, a particular schema? If there is a format that would clear your bar, I would rather build to it than invent our own and find out later that it does not.

Each account on the list must sign the message “I belong to Kerne as of [date].” Hedging should only be allowed on venues where per-address balances are public. A paid attestor is not required. Checks that anyone can run are stronger.

The completeness of the list cannot be proven. But protocol funds cannot be transferred to a hidden account without originating from a disclosed address. And anyone looking at the chain will see this. Moving funds between the disclosed protocol addresses is normal. Sending USDC to a user via redemption is normal. Transferring funds to an exchange account not on the signed list is a violation.

On July 24 you wrote: the warning stays until the hedge runs against collateral that actually backs kUSD. On July 25 the warning is gone. schema v6 now counts the founder wallet in the hedge base, which still backs no kUSD. That looks like exactly the change you said you wouldn’t make. What am I missing?

You are right. There are three claims in your last paragraph; two are correct and the third was correct when you wrote it.

“schema v6 now counts the founder wallet in the hedge base, which still backs no kUSD.” Correct. That is what commit 4b8c9e28 did.

“That looks like exactly the change you said you wouldn’t make.” It is that change, not something adjacent to it. I wrote “we are not going to” at 22:48:42 UTC on July 23. The commit landed at 23:46:12 UTC the same evening, 57 minutes and 30 seconds later, and the bot carrying it restarted at 00:16:36 UTC on July 24. Two workstreams running in parallel, one writing the sentence and one shipping the change, and nothing in the process that would have made either notice the other. That is how it happened. It is not a defence of it.

“On July 25 the warning is gone.” True when you wrote it. It is not true now, and the way it came back is worse than the thing you were complaining about.

The denominator schema 6 introduced was that wallet’s native ETH plus WETH balance. Native ETH in a spendable account is a gas balance, so the denominator moved whenever the wallet was used, while the short stayed where the engine had put it. Across the 115 attestations schema 6 published, the short was 0.0079 ETH in every single one, and the denominator took four values:

0.00792245   Jul 24 00:16:46 to Jul 25 04:17:35   SOLVENT
0.00792204   Jul 25 05:17:37                      SOLVENT
0.00792155   Jul 25 06:17:38 to Jul 26 17:29:57   SOLVENT
0.00562139   Jul 26 18:29:58 onward               WARNING_DELTA

Every one of those moves was the wallet, not the hedge. The first three stayed inside the 5 percent tolerance. The fourth was about 0.0023 ETH, crossed it, and at 18:29:58 UTC on July 26 the published status went back to WARNING_DELTA on its own, with no trade and no change in the position. You checked on July 25, in the middle of that SOLVENT run, and you read it correctly.

So the flag I said would stay was removed, and then it came back by accident. A solvency-adjacent status that moves when somebody buys gas is not a monitor. It is worse than the honest warning it replaced, because it teaches a reader to ignore the alarm.

What changed today, at 18:19:03 UTC. The attestation is schema 8 and the canonical denominator is the ETH-price-exposed leg of the attested vault alone, which is collateral that backs kUSD. It is 0.0, because vault v2 holds no user deposits, so an open short over it scores as fully directional. The status is WARNING_DELTA and it stays there until the hedge runs against collateral that actually backs kUSD.

Live at Base block 49236700, from /api/por/signed:

status                                    WARNING_DELTA
hedge_base_eth                            0.0
hedge_base_watch_only_in_canonical_base   false
offchain_net_delta                        1.0
offchain_short_position_eth               0.0079

The float is demoted, not deleted, because the engine does size against it and hiding that would be a different dishonesty. It publishes in its own fields, with its own state, and drives no status: disclosed_float_eth 0.00562139, split into disclosed_float_native_eth 0.00484848 and disclosed_float_weth_eth 0.00077291, with disclosed_float_delta 0.405345. The split is there so that next time you can tell a gas payment from a hedge change without asking me. The WETH leg never moved. The entire drop was spendable balance.

hedge_base_watch_only_in_canonical_base is hardcoded false and signed on every cycle. There is no environment variable and no code path that sets it true, and there is a test asserting there is none. That is the part worth holding me to: the commitment is a signed boolean now, rather than a sentence in a forum post, which is what it was when I broke it.

What did not change is the canonical solvency read. psm_solvency_ratio is 1.001059, from 1,113.885006 USDC against 1,112.707154 kUSD outstanding, the same figures and the same basis as on July 23. The founder float was excluded from every solvency ratio throughout, including during schema 6. The narrower commitment held. The one I actually wrote down did not.

There is now an append-only record of what every published field has ever measured, at kerne.fi/api/por/schema-changelog, and its digest is signed into every attestation. Read the schema 6 entry first. It was written on July 25, it already recorded the broken commitment and the 57 minutes, and it fixed nothing: the code went on publishing the wrong denominator for three more days while an accurate confession about it sat in the changelog. Documenting a fault faster than repairing it is only better paperwork, and the schema 8 entry says that too.

changelog_digest is 0x6e66ac3d8a188fdb9c81ab7e4c87032473c7e3d7acf222bf4398cea8053f69a0. historical_digest is 0x8728c3d6093b92079a7283d4893ca24ec974d1de3eb47f63c53579fb73fb0536, which is what the whole changelog hashed to before today. Those two standing in that relation is the check that this release appended an entry rather than edited one, and it is the check to run on us next time. The commits are 61ba333a for the bot and 2e20044f and 04061472 for the published surfaces.

On your enumeration spec, which I have not dropped. It is built to what you specified rather than to something I invented: your sentence verbatim as the message, the venue constraint enforced as a constraint rather than a flag, and no paid attestor. It is not live yet, and the attestation says so rather than implying otherwise, with account_registry_state reading “absent” and no set digest bound. Ten of the twelve accounts are contracts and cannot sign, so their membership publishes as an on-chain predicate instead, which I will set out properly when it is up. The remaining blocker is one hardware-wallet signature. I would rather it go up late than go up carrying an entry that does not verify.

Both membership signatures verify: the message recovers to 0x09a2…CA37e and 0x14f0…3946 respectively, matching the two enumerated EOAs.

One question on the basis leg, since the measurement side is now settled. The delta-neutral position earns funding while shorts pay longs. When Hyperliquid funding flips negative, the position bleeds instead. The proposal describes the hedge but is silent on this. what absorbs negative funding? the insurance fund (currently 0), the treasury (currently 0), or skUSD share price declining, meaning stakers take the loss? And is there a funding threshold at which the position is closed rather than held?

Thanks for verifying the signatures yourself. On the basis leg, the direct answer is that it is none of your three options, and I would rather explain why than pick the closest one.

The two balances first, because they are the part that matters.

Insurance fund 0xE8799FCf327C6d2F78103a3C9308c93592a30403: zero. No USDC, no WETH, no ETH.
Treasury 0x5343C41d4FF2B61DAacA9cbC050550C40605B075: zero.
Safe 0x52d3E450Ba6c299b1b07298F1E87dD74732D4877: zero.

You already had those. I am restating them because the honest answer depends on them and because the hourly signed statement has been publishing all three as insurance_fund_usd, treasury_usd and safe_usd for some time, so this is not a concession extracted from me, it is a field you can diff against yesterday’s payload.

Protocol revenue to date, for scale: 0.03 USDC of accrued swap fees sitting in the mint PSM, never skimmed. The fee destination is the treasury, not the insurance fund. A revenue route into the insurance fund is design intent and is not wired.

What absorbs negative funding today.

The margin balance in the Hyperliquid account 0x09a2780ac8Be6D5d2d1F85A8D92b09D40C9CA37e. Funding settles hourly against it and that balance falls. Account value is about $26.6 against a short of 0.0079 ETH, roughly $15 of notional. clearinghouseState on that address is a public read, so you can confirm both numbers without me.

Behind that margin there is nothing. No insurer, no guarantor, no reserve. If it were exhausted the position is liquidated at the venue and the strategy stops.

Your third option is the one I want to correct, because it is more favourable to us than the truth and I do not want it standing as our answer.

The skUSD share price does not decline, and it structurally cannot. skUSD.totalAssets() returns an internal ledger, tracked assets minus yield that has not finished vesting, not balanceOf. The only operation that reduces that ledger is a holder withdrawing their own assets, which reduces assets and shares together and leaves the price where it was. There is no write-down function, no administrative path that removes assets from the vault, and the one strategist function that can move tokens out (sweepDonations) is arithmetically restricted to the balance in excess of the ledger, so it can only release tokens that were never credited to a holder. I checked the deployed bytecode at 0x96F5102C15b839757f811A98cEc3725Ac21dFA14 against the source rather than trusting the source: every function in the source is present, and none of a long list of candidate write-down signatures appears anywhere in it.

So negative funding does not reduce staker principal. What it does is stop distributeYield from being called, which leaves the share price flat. Stakers stop earning. They are not marked down. Share price is 1.000098667771066974 kUSD as I write this.

Nor does it touch kUSD backing. Outstanding kUSD is backed 1:1 by USDC in the PSM, a different book from the venue, and a funding payment cannot move USDC out of a PSM contract. That ratio is 1113.885006 USDC against 1112.707154 kUSD outstanding, 1.001059.

The part of that which you should not find reassuring.

That separation is a property of our current stage, not a design guarantee. The basis position does not back any kUSD right now. That is exactly why the signed statement reports hedge_base_eth as 0.0 and publishes WARNING_DELTA, which is the same fact we were arguing about last week viewed from the other side. When the basis position does back kUSD, a funding loss lands on collateral standing behind a liability, and with an unfunded insurance fund there is nothing between that loss and holders. That is the condition under which the answer above changes, and I would rather write that down now than be asked about it later.

The failure mode I would watch us for is not the loss itself, it is paying a headline yield through sustained negative funding by minting kUSD that no reserve stands behind. We will not do that, and the reason to believe it is not the sentence: psm_solvency_ratio is signed hourly and recomputable from public reads, and doing it would push that ratio below 1 in the same hour.

On your second question: no. There is no funding level at which the position closes.

NEGATIVE_FUNDING_THRESHOLD = -0.0001 in sentinel_monitor.py is an alert. It writes a log line and sends a notification, and that is the entire code path. The staged reduction to 75% of nominal after 72 consecutive negative hours and 50% after 120 is specification and not deployed, listed as such under “Spec, Not Yet Wired” in our runbook and in specNotYetWired at /api/risk-status.

Two things about that threshold that you would have found, so I will say them first.

It is mis-stated in our own code comment as -8.7% APR. Minus 1bp per hour is -87.6% annualized. The comment is off by a factor of ten.

And at that level it has never fired and effectively cannot. Over the last 4,320 hourly ETH funding settlements on Hyperliquid, a full 180 days, the most negative single hour was -0.0000728, which is -63.8% annualized. The threshold sits about 1.4x further out than the worst hour in half a year. It is not a control, it is decoration, and we are recalibrating it.

The unwind paths that do exist are the loss breakers, and I should be equally plain that at our size they are also inert: the daily limit is max($50,000, 0.1% of NAV) and the hourly is max($10,000, 0.02% of NAV), so on a $15 position and $27 of margin the floors dominate and nothing can trip them. Closing this position today is a manual decision. That is the accurate description and I would rather you have it than a threshold list that implies automation.

Measured, so the answer is not theoretical.

Funding is not currently compressed. Trailing 24h averages +5.58% annualized, 7d +7.87%, 30d +9.92%, against a 180-day mean of +4.18%. 1,075 of those 4,320 hours were negative, 24.9%, and the longest consecutive negative run was 64 hours, ending 3 February.

What the account has actually realised: 252 hourly settlements since 1 May, net +$1.0997 received. 23 of them were negative and cost $0.0421 in total. That is the real magnitude of the risk you are asking about at present scale, and I think quoting it is more useful than a projection.

I am also fixing the docs, since the gap you found is real: /docs/risk-disclosures stated the effect of negative funding on yield and said nothing about who absorbs it. It will carry the waterfall above, and the insurance fund page will lead with its zero balance instead of describing an unfunded reserve in the present tense.

One question back, since your last specification became our account registry. At genesis scale the loss breakers are unreachable and a funding threshold is unreachable, so a threshold list is not a real constraint no matter where I set the numbers. What would you want to see instead from a book this small: a hard notional cap with the position closed on breach, a consecutive-negative-hours rule wired rather than specified, or something else? I would rather implement the version a reviewer would accept than the version that looks tidy on a page.

On the skUSD share price. thank you for the correction. It stays flat, it doesn’t drop.

On the threshold. I checked it: 4,320 hourly funding settlements over 180 days, worst hour −0.0000728383, and zero hours at or beyond −0.0001. So the threshold is unreachable, as you said.

But I don’t think recalibrating it helps. At genesis size any number is either unreachable or triggered by noise. The same applies to the loss breakers: max() sets the daily limit to $50,000 against a book of about $1,140.

The main point is different. Today the basis position doesn’t back a single kUSD. the backing is entirely USDC in the PSM. I rebuilt that from chain: 1,113.885006 USDC against 1,112.707154 kUSD outstanding, 1.001059, consistent with your payload. So no threshold set today protects anyone. The question becomes meaningful at the transition. when user USDC starts leaving the PSM for the strategy.

What I would suggest for that point:

The exposure limit can live in the contract. No more than N% of the backing may be deployed into the strategy at any time. No oracle is needed, because the PSM releases those USDC itself, and it is verifiable externally with the same public reads.

Accumulated negative funding does need an oracle. It would be written by you, but computed from Hyperliquid’s public per-address data, so anyone can recalculate it and catch a discrepancy.

And if there is no update for N hours, I would treat that as a trigger. Otherwise the bot going silent disables the monitoring.

contract can freeze minting and stop further USDC leaving the PSM. Closing the position stays manual.

One more question. What share of user USDC leaves the PSM at scale, and is that share fixed in the contract or does it remain policy?

Direct answer: today the share is zero. It is not fixed in the contract as a ratio, because there is no ratio to set, so in the strict sense you mean it is policy. And the one number we have published near this question is about the vault leg rather than PSM backing, and we have already published it as unenforced. I would rather give you all three of those than a clean number I picked for this post.

What “zero” actually means, since that is the checkable part.

I enumerated the exits rather than asserting them. Across the three contracts holding reserves, every path that moves a stable out is one of three:

swapKUSDForStable and swapKUSDForStableTo, a redemption, which takes kUSD in and pays stable out.
skimSurplus, which sends accrued swap fees to the treasury, bounded by skimmableSurplus.
flashLoan, ERC-3156, which pays out and takes back principal plus fee in the same transaction.

There is no fourth. No function moves reserves to a vault, a strategy or an operator. Nor is there a standing allowance that would let something else pull them, and that one is a proof rather than an observation: the ERC-20 approve selector 095ea7b3 appears nowhere in the deployed runtime bytecode of any of the three, so none of them has ever been capable of granting an allowance. vault() on the PSM is a staticcall target for a solvency read, not a route for funds.

Two things in that list you would find in one call each, so I would rather put them up front than have them read as omissions.

maxFlashLoan returns the entire balance of each PSM, and flashFeeBps is 0, so the full 1,113.885006 USDC is flash-loanable by anyone at no fee with no role gate. It is repaid inside the transaction by safeTransferFrom plus the ERC-3156 callback magic value, so it cannot leave a block and it is a different category from putting reserve into a position. But “no USDC ever leaves the PSM” would be false as stated, so I am not stating it.

And the skim is not zero either. skimmableSurplus is bounded both by accrued fees and by the balance above net backing, and live that is 0.030000 USDC on the mint PSM and 0.998000 on the retired one, 1.028000 in total. Those fees sit inside the numerator of the 1.001059 I have quoted you twice. If you recompute net of everything skimmable, the ratio is 1.000135, still above 1. I tried to break that bound arithmetically and could not: the skim cannot reach backing. But it is a bound on the fee route only, and it is emphatically not a bound on the operator, which is the next section and the one that matters more.

The reserve split, which I think is the least obvious part from outside.

0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803, live mint PSM: 30.000000 USDC
0xFf3025ec18e301855aB0f36Ec6ECa115a29A5Fbc, redeem reserve: 85.885006 USDC, plus 32.000000 kUSD
0x07eBb486e11BD217e6085eb5ab663e4517595993, retired mint PSM: 998.000000 USDC

That sums to the 1,113.885006 you rebuilt. 89.6% of the backing sits in the retired contract. On the liability side totalSupply is 1,144.707154, and the 32.000000 kUSD held by the redeem PSM is nobody’s claim, which is how you reach the 1,112.707154 outstanding you matched.

One adjacent weakness while I am listing them, because you would hit it if you tested a redemption. Each PSM funds redemptions solely from its own reserves and reverts InsufficientStableReserves otherwise. So redemption capacity is fragmented across three addresses and no single one of them covers the outstanding float. That is not what you asked about and it is not a solvency claim, but “redemptions stay open” should be scoped to “the freeze below does not close them” rather than to “redemption is unconstrained.”

Also worth pre-empting: mintingEnabled() reads true on all three PSMs, which looks alarming on a naive read. Of the three, only 0xaBDE1138 holds MINTER_ROLE on kUSD, so the other two revert on the mint branch.

I need to be exact rather than convenient about that role, because my first draft of this post said 0xaBDE1138 was the only holder and that is wrong. Replaying every RoleGranted and RoleRevoked for MINTER_ROLE on kUSD from the block the token was created, 44448090, leaves two live holders. The second is 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B, the WETH vault, granted at block 47433114 and never revoked. It has never minted: its totalSupply() is 0 and it holds 0.014315393177257526 WETH. But the capability is live, and it is a route that would mint kUSD against WETH rather than against USDC in a PSM. So “every kUSD outstanding is USDC-backed” is a statement about today’s supply and not a structural property, and you should hold me to that distinction rather than the tidier version.

On “structural”, and on staying consistent with what I already told you.

I want to be careful here because I said the opposite-sounding thing in this thread already, and I do not want to look like I am walking it back. In post 10 I wrote that the separation between the basis position and the backing is “a property of our current stage, not a design guarantee.” That still stands and I am not softening it.

The narrower claim I am making now is about these specific contracts. All three are plain non-upgradeable contracts: the EIP-1967 implementation and beacon slots are zero and there is no DELEGATECALL, no SELFDESTRUCT and no CREATE2 in the runtime. So a deployment path cannot be added to them by an upgrade. It requires deploying a new PSM and migrating reserves, which is a visible, on-chain, one-way event rather than a parameter change. That is a fact about how the change would have to arrive, not a promise that it will not.

The two things are compatible: the absence of a route in the deployed code is structural, and the separation is still stage-dependent, because the stage ends by deploying new code. There is precedent for exactly that, since the PSM has already been replaced twice with MINTER_ROLE revoked on the old instance each time.

The part of your question I would be answering dishonestly if I stopped there.

“Fixed in the contract or policy” splits into two questions, and I have only answered the first. Is there a yield-deployment egress? No, and that is a fact about the code. Is the backing safe from us? That is policy, and the honest answer is worse than the first one, so here it is before you run the two calls that find it.

DEFAULT_ADMIN and MANAGER on all three PSMs is a 2-of-3 Safe, 0x52d3E450Ba6c299b1b07298F1E87dD74732D4877, whose three owners are plain EOAs. It is not a timelock. Its guard slot is zero and getModulesPaginated returns empty, so there is no delay and no scope restriction on what it can do, and therefore no window in which an action could be seen and reacted to.

More to the point, that same Safe holds DEFAULT_ADMIN_ROLE on kUSD itself, and getRoleAdmin(MINTER_ROLE) on kUSD is 0x00. So the mint authority for kUSD sits behind the same two signatures. The supply is not operator-constrained, and the constraint that matters for backing is therefore not in the PSM at all. An answer framed only around the PSM’s own privileged surface would have been the wrong answer to your question, which is why I am giving you this one instead.

So: the deployment share is zero and the contract is the reason. The integrity of the backing rests on a 2-of-3 multisig with no timelock and no guard, and our conduct is the reason. Those are different kinds of assurance and I do not want the first lending credibility to the second.

Two smaller disclosures in the same spirit. solvencyCheckDisabled() is currently true on the live mint PSM, so the vault-side solvency gate is deliberately off in production, event-logged rather than silently failing open, but off. And the three PSMs are not the same code: 0xFf3025ec, holding 85.885006 USDC or about 7.7% of backing, is an older build whose oracles[USDC] is the zero address and whose minSolvencyThreshold is 0, so it is running without the depeg and solvency hardening the other two have.

On the published number, which I need to correct before you find the seam.

The nearest published figure is 10%, and it appears on two of our surfaces with different wording. The risk chapter scopes it: “Internal policy calls for 10% of TVL held as liquid collateral on Base for instant withdrawals. Implementation is partial; this number is not yet a hard contract constraint.” But /api/risk-status lists the same item as “10% on-chain withdrawal buffer as a contract-enforced reserve” with no scoping word, which a reader asking your question could reasonably take as a committed 90/10 split of backing. It is not. It is a vault-TVL withdrawal-buffer policy, it has never applied to PSM backing, and the API wording is looser than the docs wording. I am going to tighten the API line rather than leave you to reconcile them.

So: no number has ever been published for share of backing deployed. That commitment is clean, which is why I would rather set it with you than announce one.

Your four suggestions, in order.

The exposure limit belongs in the contract and you are right that it needs no oracle. The PSM is the party releasing the USDC, so it can enforce the constraint on itself at the moment of release, and the result is checkable with the reads you already used. Two changes to the shape. First, I would write it as a floor on reserves that must remain rather than a ceiling on what may be deployed. Those look equivalent and are not: a percentage ceiling has backing as its denominator, and redemptions shrink that denominator, so a book that was inside the limit drifts outside it with no deployment taking place and nothing to point at. A floor is a statement about what a redeemer can always get, which is the property that matters, and it fails in the right direction. Second, it has to exist before the first dollar moves. A limit written once there is something to protect gets written under exactly the pressure that produces a generous number.

The funding oracle is right, and the account registry is what makes it checkable. Your version works because the PoR signer is itself the Hyperliquid account, so per-address attribution is a signature from the account rather than a claim about it, and clearinghouseState on that address is a public read anyone can run. Cumulative funding is a natural additional field. The residual weakness is not the arithmetic, it is completeness, the same gap I conceded earlier: nothing proves that account is the only one. The signed registry exists for that reason, currently version 2, as of 2026-07-28, 15 entries. Your oracle is only as recomputable as that registry is complete, so I would treat them as one piece of work rather than two.

The staleness trigger is the sharpest thing in your post and I have no argument with it. A monitor that fails silent is not a control, and it is the same defect as an unreachable threshold arriving by a different route. The attestation already publishes generated_at hourly, so staleness is externally measurable today, by you, without us. What is missing is that nothing acts on it. That asymmetry, where the outside world can detect the failure and the system cannot, is the wrong way round.

On freezing, the capability exists today but it is not a contract response, so on your axis it is policy too. The cleanest lever is setStableCap(USDC, 0): one MANAGER_ROLE transaction, it blocks new mints at the cap gate, and the redeem path never reads stableCaps, so redemptions stay open. That is the honest match for what you are describing. I should be precise about the other two rather than offer them as equivalents. setMintingEnabled(false) is a mode switch, not a halt: it takes a branch that dispenses pre-held kUSD inventory instead of minting new supply, so it always freezes new supply but only halts the swap because the live mint PSM’s kUSD inventory happens to be zero. And pause() is the blunt one, because all four swap functions carry whenNotPaused, so pausing to protect backing would also stop holders redeeming. “Stop further USDC leaving the PSM” splits into two different things depending on whether it is leaving toward a strategy or toward a holder, and only the first should ever be frozen.

And on your main point, which I think is correct.

You are right that no threshold set today protects anyone, and right that recalibrating is not the fix. max() setting the daily loss limit to $50,000 against a book of about $1,140 is not a control, it is a number that cannot bind, and the same would be true of anything I picked for the funding threshold. The useful consequence is that the constraint has to be defined in terms of the transition rather than in terms of today’s book, which is what your reserve floor does and what a threshold list does not.

Two things I would want your view on, since you have thought about the transition more carefully than anyone else who has looked at this.

First, a reserve floor protects redeemers but says nothing about what the deployed portion is doing. Would you want the floor alone, on the grounds that it is the only part cheaply verifiable from outside, or the floor plus a hard notional cap on the basis position, so the deployed portion has a ceiling that does not move with the book? I lean toward both, but the second is the one I can imagine getting wrong.

Second, and I think this one now outranks it. Everything you proposed moves a constraint from policy into a contract, and the section above is me admitting that the largest constraint of all, whether the backing stays where it is, is still entirely policy and has no delay in front of it. An exposure floor written by an operator who can grant itself the mint role is a floor with a door beside it. So if you were sequencing this: does the mint authority get a timelock or a scoped guard before the exposure floor gets written, or do you consider those independent enough to do in either order? My instinct is that the floor is worth less than it looks until the first one is done, and I would rather hear that I am wrong about the ordering than find out later that I was right and did it backwards.

First, I’d like to apologize for yesterday’s post; it was a draft from another thread, pasted by mistake.

I verified your Safe claims directly. guard slot zero, no modules, 2 of 3, all owners EOA, DEFAULT_ADMIN on kUSD at Safe, getRoleAdmin(MINTER_ROLE) = 0x00. All matched.

Now I want to answer your question about the order. As long as mint authority is under two signatures, you can mint as much as you want. Floor guards the reserves, and the threat isn’t in the reserves. mint inflates existing kUSD without touching the reserves. So, first the key, then the floor.

A timelock without a mechanism only delays what can’t be prevented. There’s no one to vote. So, the reaction is automatic. a pending mint-role change in the timelock queue → mint closed, redemption open. Entry closed, exit open. window mechanism = setStableCap(USDC, 0), automatic. otherwise the timelock repeats the staleness asymmetry you described.

Now your question is whether it’s worth installing just the floor or both. I think both, because they solve different problems. floor guards the exit of those inside; cap limits the size of the trouble outside. the cap as a constant, changed only through redeployment and migration.

Inside the window - would you treat the automatic contract response (mint closed, redemption open) as the mechanism, or do you see a different one?

Yes, the automatic contract response is the mechanism I would build. I want to accept it properly rather than just agree with it, so this is in three parts: what moved on chain since you wrote, why the trigger you named is not sufficient on these specific contracts, and what I will and will not commit to.

Two transactions this morning, because you will find them if you look at the Safe again.

At 2026-08-03 04:01:31Z, block 49470172, tx 0x36f7b520fe2071121da65e5690ca21b0df0df35b5ae1bd19e3d83a4e8d3bf6de, the 2-of-3 revoked MINTER_ROLE on kUSD from 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B, the WETH vault. That is the second holder I disclosed to you in post 12. The minter set is now exactly one, the live mint PSM 0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803.

Please do not take that from hasRole. kUSD is plain AccessControl rather than AccessControlEnumerable, so getRoleMemberCount is absent from the runtime and reverts, and hasRole only answers for an address you already name. Testing a handful of candidates is exactly how I published a wrong minter count in this thread in the first place. The check that settles it is a replay from the token’s creation block, 44448090:

cast logs --from-block 44448090 --to-block latest \
  --address 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 \
  "RoleGranted(bytes32,address,address)" --rpc-url <base-rpc>
cast logs --from-block 44448090 --to-block latest \
  --address 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 \
  "RoleRevoked(bytes32,address,address)" --rpc-url <base-rpc>

Filter to role 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6 and net grants against revokes. Fourteen role events over the token’s life, eleven of them MINTER, netting to one holder today. DEFAULT_ADMIN nets to one, the Safe. Most public RPCs cap eth_getLogs at 10,000 blocks, so it needs chunking.

The second transaction, four and a half minutes later, is a loosening, and I would rather hand it to you than have you find it. Tx 0x3e4c4d0c0fd33ddbb2d45b4d10a58d15faebe284d6301c57c7a3fbb529562989 set maxOracleDelay(USDC) on the mint PSM from 93,600 seconds to 172,800. The feed is Chainlink USDC/USD 0x7e860098F58bBFC8648a4311b374B1D669a2bc6B. I measured fifteen consecutive round gaps at 24.003h to 24.008h with no deviation-triggered updates, so it is a pure 24h heartbeat, and the old window left under two hours of margin before one skipped heartbeat would have reverted every mint and every redemption with OraclePriceStale. 172,800 is the contract’s MAX_ORACLE_DELAY_BOUND; 172,801 reverts. So it buys liveness and pays for it with a gate that now reacts up to 48h late in a real depeg instead of 26h. At a book of 1,113.885006 USDC I think that is the right trade, but it is a trade and not an improvement, and it is the staleness asymmetry you have been pointing at all along, sitting on the other side of the ledger.

Where I have to disagree with you, and it makes your argument stronger rather than weaker.

You wrote: first the key, then the floor. I agree, and I want to be exact that the revoke above is not that. It narrows the minter set. It does not constrain the key. getRoleAdmin(MINTER_ROLE) on kUSD is still 0x00, DEFAULT_ADMIN still sits with the same 2-of-3, and it can grant a new minter in one transaction with no delay. The honest sentence is “one minter today, changeable by two signatures”, not “one minter”.

But here is the thing I would have got wrong if I had simply built what you described. Your window opens on a pending mint-role change. On these contracts a mint-role change is not the only way to mint, and it is not the cheap way.

MANAGER_ROLE on the mint PSM is a second, independent path, and it never touches grantRole:

  1. addStable(EVIL, 0, type(uint256).max), onlyRole(MANAGER_ROLE)
  2. setOracle(EVIL, EVIL_ORACLE), onlyRole(MANAGER_ROLE)
  3. swapStableForKUSDTo, permissionless, and the PSM already holds MINTER_ROLE, so the PSM mints
  4. swapKUSDForStableTo, permissionless, takes the reserve out

Both setters are in the deployed runtime, selectors 0xeee7c155 and 0x5c38eb3a, both MANAGER gated, and the Safe holds MANAGER on all three PSMs today.

I did not reason that out, I ran it. Against a fork of live Base state, completing exactly the sequence of putting kUSD’s DEFAULT_ADMIN behind a 48h timelock and having the Safe renounce its own, so the Safe provably holds zero kUSD admin, and then draining anyway:

anchor deposits 1,000,000 USDC  ->  999,500 kUSD
=== Safe now holds no kUSD admin ===
attacker USDC balance           ->  989,336.679410
reserve after attack            ->   10,693.320790
anchor still holds kUSD         ->  999,500

So a timelock scoped to the mint role would have been decorative. Worse, renouncing kUSD admin while keeping PSM MANAGER is strictly worse than doing nothing, because it also surrenders the ability to instantly revoke MINTER_ROLE from a compromised PSM, and bounds nothing in exchange.

None of that damages your design. It tells you what the window has to key on. Watching for a queued role change catches one of two paths. The trigger set has to be every privileged call that can end in a mint, which here means kUSD’s DEFAULT_ADMIN and MANAGER on all three PSMs, not the mint role alone.

On “automatic”, which I think is the word doing the most work in your post.

There are two ways to build “queued change closes minting”, and only one of them is what you mean. If a keeper watches the timelock and calls setStableCap(USDC, 0) when it sees something queued, that is a bot, and it fails silent, which is the exact defect you named about the funding oracle. The version that earns the word automatic is the mint path reading the pending-change state itself, so entry closes because the state is true and not because something noticed. setStableCap(USDC, 0) stays the right lever for a deliberate freeze and it is what I told you in post 12, but as the window mechanism it has to be a read on the way in, not a write from outside.

Two honest limits on “exit open” while I am here, because I do not want to accept a property I cannot currently deliver. Redemption capacity is fragmented: each PSM funds redemptions only from its own reserves and reverts InsufficientStableReserves, and no single one covers the outstanding float. And pause() puts whenNotPaused on all four swap functions, so the blunt lever closes the exit too. “Exit open” is something I would have to build, not something I can claim today.

There is also a mechanical trap in scoping the timelock, which is why this is not one transaction. pause and addStable share MANAGER_ROLE, OpenZeppelin cannot split a role, and the deployed PSM has no setRoleAdmin (selector 0x1e4e0091 is absent from the runtime, and getRoleAdmin returns 0x00 for every role), so MANAGER cannot be re-parented. Put MANAGER behind a 48h delay naively and the emergency stop goes behind 48h with it. The shape that survives is a guardian contract holding MANAGER that exposes exactly one function, pause(address), gated to the Safe and restricted to three constructor-fixed addresses, with no generic execute and no way to widen the target set. unpause stays on DEFAULT_ADMIN and therefore takes the delay, which I would publish as a property rather than a caveat: it removes our ability to pause an exit and then instantly resume once we have done whatever we paused for.

Floor and cap.

I agree with both, for your reason, that they fail differently. On the cap as a constant changeable only by redeployment, I would take that, and I want to name its cost so I am not agreeing cheaply. If the only way to raise it is a migration, then every raise is the riskiest operation a small protocol performs, so the number has to be set high enough that the first raise is not near, which means it binds later than it looks. The honest version of that promise is “this number cannot be raised quietly”, not “this number is small”.

What I will and will not commit to.

The timelock plus guardian design is built, and nine of nine fork tests pass against live state, delay at 48 hours. It is not deployed and it is not proposed to the Safe, and I am not going to give you a date, because co-signing is three people’s calendars and the two transactions above sat at one of two signatures for twenty eight hours. What I will commit to is the order, because a half-applied handover is the failure mode: grant everywhere, verify, then renounce, and renounce kUSD admin last, since that is the lever that can re-grant the rest.

I will not commit to the exposure floor before the custody work, on your own argument. A floor written by an operator who can still mint is a floor with a door beside it.

A question back, since you have thought about the window more carefully than I have.

Should it key on the queue or on the state? Keying on the queue means one queued call closes entry immediately, which is clean, but it also means anyone who can propose can close minting, so proposal rights become a denial-of-service surface. Keying on the state means entry closes only once the change has landed, which is too late to be a window at all. I do not have a good third option and I suspect you do.

Separately: thank you for running the Safe checks yourself. All six of the things you listed were things I had asserted in this thread, and an assertion someone else has recomputed is a different object from the assertion. It is the one part of this thread I could not have produced on my own.

I also think a queue with a limited set of triggers is better.

You need to create a trigger set for only privileged operations that can result in a mint. You’ve already listed them in this thread(grantRole on kUSD plus addStable/setOracle on PSM). Queuing harmless parameters shouldn’t close the mint.

If PROPOSER is the same Safe 2-of-3 as EXECUTOR, then only the operators themselves can spam, which is not an attack surface. Therefore, proposer rights shouldn’t extend beyond the executor set.

OZ TimelockController stores operations as ID hashes, so it’s impossible to cheaply list pending operations on-chain. Therefore, a thin wrapper with a pendingPrivilegedOps counter is needed. This is incremented when queuing an operation from the trigger set, decremented when executing or canceling. And the mint path requires the counter to be zero, otherwise it will revert. Then the window is a state readout, automatic, without a keeper, without silent failure.

PROPOSER set can’t be read on-chain until the timelock is deployed, no contract, no RoleGranted events to replay, so this is the one part I can’t settle by measurement. who holds the PROPOSER_ROLE in a build that passes nine fork tests? Only Safe 2-of-3, or is the Guardian also in the Proposer set?

Only the Safe. PROPOSER_ROLE, EXECUTOR_ROLE and CANCELLER_ROLE are all held by the same 2-of-3 and by nothing else. The Guardian is not in the proposer set, and it is not in the executor or canceller set either.

You can also settle it by measurement now, which you could not when you wrote. The timelock was deployed at 2026-08-03T17:53:21Z, block 49495127, twenty four minutes after I posted that it was not deployed. That sentence was true when I wrote it and stopped being true the same evening, and I would rather hand you the gap than have you find it.

0x36A14976980B7Dd33136f6613545EB0A2C0a0D72. eth_getCode is 0 bytes at block 49495126 and 6,536 bytes at 49495127, so that is the creation block and the replay has a floor:

cast logs --from-block 49495127 --to-block latest \
  --address 0x36A14976980B7Dd33136f6613545EB0A2C0a0D72 \
  "RoleGranted(bytes32,address,address)" --rpc-url <base-rpc>

Four role events over the contract’s entire life, all four emitted inside the deployment transaction 0xe664ab3f857492fd7ecb1c621d32a7c742595b040890282c35eda9a2976394c5, and none since. No RoleRevoked at all.

role hash holder
PROPOSER_ROLE 0xb09aa5ae...6819cc1 Safe 0x52d3E450
EXECUTOR_ROLE 0xd8aa0f31...51469e63 Safe 0x52d3E450
CANCELLER_ROLE 0xfd643c72...5726f783 Safe 0x52d3E450
DEFAULT_ADMIN_ROLE 0x00 the timelock itself

I read that on drpc, mainnet.base.org and the Tenderly gateway and they agree on the netted set and on the event count. Most public endpoints cap eth_getLogs at 10,000 blocks; Tenderly’s serves the whole range in one call.

The last row is the one I would check rather than the first. getRoleAdmin returns 0x00 for all four roles, and DEFAULT_ADMIN_ROLE on the timelock is held by the timelock and not by the Safe and not by the deploy EOA. That is the constructor’s admin = address(0) showing up as an observable, since OZ 5.x grants DEFAULT_ADMIN_ROLE to address(this) unconditionally and to admin only when it is non-zero. So proposer rights cannot quietly extend beyond the executor set: adding a proposer is itself a scheduled operation carrying the full 48 hours, proposed by the Safe and cancellable by any canceller, which today means the Safe and nobody else. The property you asked for holds because widening is delayed, not because we say we will not do it.

Now the part of that answer that is not good news, which I would rather write than have you compute.

The Safe holds proposer, executor and canceller simultaneously. There is nobody outside the operator set who can stop a queued operation. The delay is notice, not veto. It buys a holder two days and a public queue and it constrains us not at all at the end of those two days. The thing that would change that is a canceller seat held by someone who is not us, and no seat exists, because nobody holds one.

Item three landed, about eight hours after you posted.

Executed 2026-08-06T17:00:07Z, block 49623130, tx 0xed24d495789540dae543793a5d8c9d884d8abfc386c0e726084da9183933d9b8. Thirteen calls in one timelock batch, operation 0xb1fdeff1179b626dfbd3cd1a8b5fe93fda32a1056f01eea424d72db889155957, scheduled 2026-08-03T23:28:05Z and executable from 2026-08-05T23:28:05Z.

DEFAULT_ADMIN revoked from the Safe on kUSD and all three PSM modules. MANAGER revoked from the Safe on all three. Both granted to the timelock, and MANAGER granted to the Guardian in the same atomic batch, so there was never a block without an emergency stop.

One ordering note, because it is the part I would have got wrong following my own post 16. I said grant, verify, then renounce. We did not renounce. The timelock revoked the Safe from inside the scheduled batch. If the timelock had been broken the batch would simply never have executed and the Safe would still hold everything, whereas renouncing first and discovering the timelock is broken second leaves governance bricked with the roles already gone. The step that removes the Safe’s power is now the same step that proves the timelock works.

What did not move, in the same breath rather than three paragraphs down.

skUSD 0x96F5102C15b839757f811A98CEc3725Ac21DfA14 holds 1,011.582169 kUSD of 1,144.707154 outstanding, 88.37%, and the Safe still holds sole DEFAULT_ADMIN over it with no delay. VaultV2 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B is the same, DEFAULT_ADMIN and PAUSER with the Safe. So the accurate sentence is that mint and redeem admin sits behind 48 hours and the staking vault does not. Anything shorter than that is false and takes one call to refute:

cast call 0x96F5102C15b839757f811A98CEc3725Ac21DfA14 \
  "hasRole(bytes32,address)(bool)" \
  0x0000000000000000000000000000000000000000000000000000000000000000 \
  0x52d3E450bA6c299B1B07298F1E87DD74732D4877 --rpc-url <base-rpc>
# true

The handover made one thing worse, and it is the thing your window needs.

setStableCap is onlyRole(MANAGER_ROLE). MANAGER now sits with the timelock. So the surgical lever, close entry and leave the exit open, now takes 48 hours to pull. Simulated against live state today, eth_call only:

setStableCap(USDC, 0)      from Safe       -> revert 0xe2517d3f AccessControlUnauthorizedAccount
setStableCap(USDC, 0)      from timelock   -> ok
setMintingEnabled(false)   from Safe       -> revert 0xe2517d3f
pause()                    from Safe       -> revert 0xe2517d3f
PauseGuardian.pause(PSM)   from Safe       -> ok

The timelock rows say only that the access check passes when the caller is the timelock. Making the timelock the caller is the 48 hours.

The only instant lever left is pause, and pause puts whenNotPaused on all four swap functions, so it closes the exit along with the entry. After today every write-based version of your window is either delayed or blunt. I would make the same trade again, but it does remove the fallback I was implicitly leaving myself in post 16, and it means the read on the way in is not the better of two shapes any more. It is the only shape that delivers mint closed and redemption open on these contracts.

On the wrapper with pendingPrivilegedOps. It is not built, and I want to name the obstacle rather than agree and go quiet.

The counter only does anything if the mint path reads it. The mint path is _swapStableForKUSDInner on 0xaBDE1138, and that contract is not upgradeable: EIP-1967 implementation, admin and beacon slots and the EIP-1822 proxiable slot all read zero on the PSM and on kUSD.

I did look for a way to smuggle the predicate in without a redeploy, because it would be cheap if it worked. The mint path’s gates are supportedStables, _checkDepeg, _checkSolvency, stableCaps and whenNotPaused. Four of those are local state. The one exception is the oracle behind _checkDepeg, whose address is operator settable through setOracle, so an oracle that fails when the counter is non-zero would in principle close entry with no new PSM. It does not work, and the reason is the same one that makes pause blunt: _swapKUSDForStableInner calls _checkDepeg(stable) and _checkSolvency() on exactly the same terms as the mint path, so a predicate hidden in the oracle closes redemption in the same block it closes minting. Every operator settable address in the entry path is also read by the exit path. There is no asymmetric lever left on this contract.

So it is not a thin wrapper added beside the timelock. It is a new PSM and a migration of the mint path onto it, which is the same cost class you accepted yourself for the cap when you said changed only through redeployment and migration. The exposure floor from post 11 lands in exactly the same place, an in-contract limit on the same immutable contract, so those are one piece of work and not two. I am not going to give you a date, for the reason I gave last time.

One correction to your premise, which makes the audit side cheaper than you assumed. You are right that OZ stores operations as id hashes and that storage cannot enumerate them. But CallScheduled, CallExecuted and Cancelled are all indexed by operation id, so the pending set is recoverable from logs even though it is not readable from state. Over the timelock’s life: 13 CallScheduled, all belonging to the one operation above, 13 matching CallExecuted, zero Cancelled. The queue is empty right now. That does not remove the need for the counter, since a log replay cannot gate a mint, but the view you would use to check the counter against reality already exists.

Three things in the Safe queue you will see, so here they are first.

Nonces 27, 28 and 29, each at one of two signatures, each addressed to the timelock. They schedule unpause on the three PSM modules. The reason is the asymmetry above: pause is instant through the Guardian and unpause is DEFAULT_ADMIN and therefore 48 hours, so a pause costs two days of downtime unless the restart is armed in advance. They are three independent operations rather than one batch because executeBatch is atomic and unpause() reverts ExpectedPause on a contract that is not paused, so a single three module batch would only fire if all three were paused at once, which is not the realistic incident.

The property that makes pre-arming safe is checkable right now against the live PSM: unpause() from the timelock reverts 0x8dfc202b ExpectedPause, not 0xe2517d3f. The access check passes and the state check fails. A pre-armed unpause cannot fire while the system is healthy.

Built, versus signed, versus designed, versus not written.

On chain: the timelock at 172,800 seconds, the Guardian, the handover.
Signed and queued, not executed: the three standby unpause operations, waiting on a second signature.
Designed and tested, not deployed: the canceller seat, one immutable holder, one immutable expiry, forwarding only cancel(bytes32). Not deployed because there is nobody to hold one.
Not written: the pendingPrivilegedOps wrapper, the read on the mint path, and the exposure floor.
Not delivered: exit open as a property. Redemption is still funded per module. 1,110.888006 USDC across the three PSMs against 1,144.707154 kUSD outstanding, and the largest single leg is 995.003 sitting in the retired v2, so no single module covers the float.

Guardian 0xC47adaf51907bB1871D07E18eA21dc75Ae93Cc8E, 1,012 bytes, fully verified on Blockscout with an exact metadata match. Its runtime contains pause(address), safe(), module0(), module1(), module2(), and does not contain unpause(address), execute(address,bytes) or owner(). The timelock is partially verified there, source matching with a differing metadata hash.

A trap in checking that, which caught me today. The Guardian holds MANAGER on all three modules, so an eth_call with from set to the Guardian passes onlyRole(MANAGER_ROLE) on addStable, because eth_call does not execute the caller’s code. The role check alone does not bound it. What bounds it is that the Guardian’s own bytecode can express exactly one call, a hardcoded pause() to one of three constructor-fixed addresses, with no generic execute. Read the runtime rather than take that from me. PauseGuardian.pause(module) from any address other than the Safe reverts 0x9dc4246e NotSafe, including from the timelock.


Separately, and unrelated to everything above.

The report at hexens.io/audit-reports/kerne-protocol-july-2026 links its scope to five files in a private repository, plus the remediation commit as a sixth link. All six return 404 for every reader, which I re-confirmed today. That is our problem rather than the auditor’s, and it left the commit hash as the only checkable thing in the scope section. The reviewed source is now published verbatim, so those links have somewhere to resolve:

report link (404) resolves to bytes sha256
.../kerne-main/blob/0912c870.../src/KerneVault.sol contracts-public/audits/scope/0912c870/src/KerneVault.sol 129,521 243c554f...5dd2adde
.../src/kUSD.sol same directory, kUSD.sol 5,275 dfc3339c...1de894a4ca
.../src/skUSD.sol same directory, skUSD.sol 10,421 7a1c9a2d...f89eff50c4
.../src/KUSDPSM.sol same directory, KUSDPSM.sol 45,019 013bd7e6...2d7c963b99f9
.../src/esKERNE.sol same directory, esKERNE.sol 32,694 c7d7f0cf...74c7424875
.../kerne-main/tree/98f29e55... contracts-public/audits/scope/98f29e55/ tree remediated

github.com/kerne-protocol/contracts-public/tree/main/audits/scope

Diffing the two directories is the cheap check on the report’s dispositions. Only KerneVault.sol differs between them, 12 hunks, 120 lines added and 60 removed, 2,196 to 2,256 lines, no function added or removed. The other four are byte identical at both commits. That reproduces the report’s claim that all ten findings sit in the vault out of code rather than out of our summary of it.

One bound that has to travel with it. None of that is the bytecode running on Base. The live vault 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B was created at block 47378986, 2026-06-15T18:15:19Z, from source older than the reviewed commit, so the vault findings are open against the deployed one and not against either directory above. What bounds that today is checkable rather than asserted: totalSupply() is 0, so it has never issued a share, and whitelistEnabled() is true, so maxDeposit returns 0 for any address not on its list. I would not read a zero out of maxDeposit alone, since that is a whitelist answer about the address you asked about and not a statement about the vault.

The single file where reviewed and deployed coincide is the PSM. The live mint module’s verified source is byte identical to the audited KUSDPSM.sol, both 45,019 bytes and sha256 013bd7e6fc2bcb5e780aaa5f924a905f3f63e88bee49d1bf3d832d7c963b99f9, with Sourcify reporting exact_match on creation and runtime and Blockscout reporting a full verification. So the contract on the live mint path runs exactly the reviewed code, and it drew no findings. That is the reason the vault findings are survivable, and it is also the whole of the good news, so I will not stretch it further than that.

I verified everything as stated. The deploy tx carries exactly four RoleGranted events: proposer, executor, and canceller on Safe, DEFAULT_ADMIN on the timelock itself, and minDelay set to 172800 upon creation. Deployer 0x14f0… this is the second EOA from the account registry whose membership signature I recovered earlier. So the attribution of the timelock deployment to your exposed keyset is closed through the verification I’ve already performed. 13 calls match. Guardian receives MANAGER at indexes 1, 3, and 5, before Safe revokes at indexes 6–11. never a block without an emergency stop, confirmed by event ordering within the transaction, indexes 1/3/5 before 6–11.
One precision note on the batch. none of the six grants in it are DEFAULT_ADMIN, all six are MANAGER. Yet the sender on every revoke is the timelock itself, and only the role admin can revoke, so DEFAULT_ADMIN on kUSD and the three PSMs was granted to the timelock earlier, in separate transactions. From those grants until execution on Aug 6, the Safe and the timelock held admin simultaneously. Consistent with what you did. slightly tighter than ‘both granted… in the same atomic batch’

Guardian: runtime 1012 bytes, as stated. pause(address) to three hardcoded addresses via raw call 0x8456cb59, NotSafe error decoded to 0x9dc4246e. There’s no execute in any signature, no unpause, no grantRole, no privileged PSM selectors. The timelock address appears nowhere in the runtime. the dispatch table has six externals, the sixth is 0xbabcc539, isAllowed(address), a public view of the module predicate. Harmless.

On skUSD: hasRole(0x00, timelock) = false. The staking vault remains outside the handover, as specified.

The live mint PSM isn’t verified on BaseScan and Read Contract is unavailable, only bytecode. Sourcify and Blockscout are fine, but a reviewer opening the default Base explorer will see raw bytecode. Cheap to fix.

48 hours is a notification only if someone receives it. Today, nothing monitors CallScheduled on this timelock: no subscriber, no alert, no public page. The queue is recoverable from logs, but no one is recovering it. A canceller learning of an operation at hour 47 is decoration, whoever holds the seat.

You are right about the batch, and post 18 was wrong on it. Correcting it here rather than leaving it to sit in the thread.

Post 18 said DEFAULT_ADMIN and MANAGER were “both granted to the timelock, and MANAGER granted to the Guardian in the same atomic batch”. Only MANAGER was in the batch. All six grants inside 0xed24d495789540dae543793a5d8c9d884d8abfc386c0e726084da9183933d9b8 are MANAGER_ROLE, three to the timelock and three to the Guardian, in the order you have them: Guardian at call indexes 1, 3 and 5, the six revokes at 6 through 11, and the thirteenth call is the DEFAULT_ADMIN revoke on kUSD.

DEFAULT_ADMIN reached the timelock three days earlier, in four separate transactions, each of them a Safe execTransaction wrapping a single grantRole(0x00, 0x36A14976):

contract block UTC tx
kUSD 0x5C2EfdF0 49505050 2026-08-03T23:24:07Z 0x592ed40f27e74c0b06d67b5c7e72cea6c1e5821f459a62bff02e8732b1e8cc7c
PSM, live mint 0xaBDE1138 49505132 2026-08-03T23:26:51Z 0xc9028c20d17da2f4bc73de009483cb74998e1364c7335e188e770437b042f13c
PSM, retired mint 0x07eBb486 49505163 2026-08-03T23:27:53Z 0xe924f1fb04bbf6aa9035b98f0c25d43a5b3ad922b2f106b0ea6a0b4fbe983325
PSM, redeem reserve 0xFf3025ec 49505165 2026-08-03T23:27:57Z 0x28f70b0dbb12e5f4143b4ec08dece1c6ca591b0cc91c5d71b016f718b5270ef4

So the window you inferred is real, and it is the part I should have published rather than leaving you to derive it. On kUSD it runs from 2026-08-03T23:24:07Z to the batch at 2026-08-06T17:00:07Z, two days seventeen hours and thirty six minutes, during which the Safe and the timelock both held DEFAULT_ADMIN. On the three PSM modules the same overlap starts at their own grant timestamps above, so it is three to four minutes shorter on each. The delay did not bind for that period, because the Safe could still act directly. Your version is tighter than mine and it is the correct one.

Replayable:

cast logs --from-block 0 --to-block latest \
  --address 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 \
  "RoleGranted(bytes32,address,address)" \
  0x0000000000000000000000000000000000000000000000000000000000000000 \
  0x00000000000000000000000036A14976980B7Dd33136f6613545EB0A2C0a0D72 \
  --rpc-url <base-rpc>

Most public endpoints cap eth_getLogs well below that range. The Tenderly gateway serves it in one call, which is how I read it.

Two precisions on that table, both of which you would hit if you replayed it yourself, so they belong here rather than in a reply to you later.

First, the Safe is the msg.sender on each grant, which is what RoleGranted.topics[3] records, but it is not the tx.from. All four were submitted on chain by 0xbd6c548cb909a52a9ef138d563f406ff677226cf, with the Safe as tx.to, and the same address submitted the 08-06 batch. That EOA has no code, six transactions in total, and used nonces 0 through 3 for exactly these four. It is not one of the Safe’s three owners, which are 0x14f04cE0, 0x0CeC7021 and 0xf4A43C88 at threshold 2. It is a relayer, and it holds no authority: execTransaction verifies owner signatures carried in its own calldata, so whoever pays the gas cannot change what executes. Worth saying because an auditor looking for the Safe in a from column will not find it there.

Second, four is the count of Safe-sourced grants. Chain wide there is a fifth RoleGranted(DEFAULT_ADMIN, 0x36A14976), which is the timelock granting the role to itself in its own deployment transaction at block 49495127. That is the constructor behaviour I described in post 18, not a separate act.


On BaseScan, I cannot reproduce what you saw, and I am not going to claim I fixed it.

Read at 2026-08-08T03:52:35Z, 0xaBDE1138 renders Source Code Verified, Exact Match, contract name KUSDPSM, v0.8.24+commit.e11b9ed9, optimizer enabled at 1000 runs, cancun, eighteen files under Solidity Standard Json-Input, with both Read Contract and Write Contract tabs present. kUSD, skUSD, the other two PSM modules, the vault, the timelock and the Guardian all return the same shape, and none of the eight serves the unverified contract prompt.

I did not touch verification today and I cannot date it from outside, so I will not tell you it was fixed in response to your post. Either it changed between your read and mine, or we were on different addresses. If you are still seeing raw bytecode, give me the address and I will chase it.


Your second item is correct and it is not fixed.

Nothing subscribes to CallScheduled on 0x36A14976. No alerting, no subscriber, no public page. The queue is empty as of tonight, thirteen CallScheduled, thirteen CallExecuted, zero Cancelled, one operation over the contract’s whole life, and the only reason anyone can say that is that I ran the replay to answer you. A log replay is a thing a reader can do; it is not monitoring, and I am not going to blur those two. I am not giving a date, for the same reason I did not in posts 16 and 18.

The stronger form of your point, which I would rather state than leave implicit: a monitor would still hand a holder nothing to do at hour 47, because CANCELLER_ROLE is the Safe and nobody else. Watching only becomes worth something once a seat exists outside the operator set to act on what is seen. Neither the monitor nor the seat exists. The seat is designed and not deployed, and it is not deployed because there is nobody to hold it.


Two small confirmations, since you measured them.

The deployer is 0x14f04cE02f35B29Af564A98544dD7e2393993946 and it is one of the three Safe owners. Nothing about that attribution is contested on our side: we publish that address ourselves at kerne.fi/api/reward-capacity, labelled as the founder hardware wallet. I would not call the key exposed, it is a hardware wallet and it is disclosed deliberately, but you are right that it is attributable and that the timelock deployment and the Safe share a signer.

And the sixth external on the Guardian is 0xbabcc539, which is isAllowed(address), a public view over the module predicate. The runtime carries it alongside pause(address), safe() and module0() through module2(), and nothing else in that dispatch table writes state.

checked 0xaBDE1138 again today, 2026-08-09 13:30 UTC: source code verified, exact match, KUSDPSM, v0.8.24+commit.e11b9ed9, optimizer 1000 runs, cancun, both read and write tabs present. matches your read.

on the batch: accepted. the four separate DEFAULT_ADMIN grants and the overlap window account for everything i measured. nothing further on it.

stepping back, since this thread has reached a natural boundary. what it established: the canonical solvency read is the PSM ratio, excluding off-chain assets; the hyperliquid balance is publicly readable and signed by the account itself, but completeness of the account set is not provable on-chain. negative funding is absorbed by venue margin with no insurance fund and no automatic close; the minter set was narrowed to one. mint and psm admin moved behind a 48h timelock with an instant scoped pause guardian. what remains open is documented in the thread: the canceller seat, queue monitoring, the exposure floor, the pending-ops check on the mint path, exit-open as a property. those are build items now, not open questions. as of block 49746357: timelock queue 13 scheduled / 13 executed / 0 cancelled, safe nonce 27. nothing further from me until there is new on-chain state to verify

You said nothing further until there is new on-chain state to verify. There is, and some of it was already on chain when you took your reading. Nothing below needs a reply.

1. An isolated kUSD/USDC market on Euler, deployed 2026-08-09

borrowable USDC vault, eUSDC-116 0xe87c294E1139C31770727193e3Be0ccEd73d6AA3
escrow collateral vault for kUSD, ekUSD-1 0xD3800ceb6bBeB90101ed5d5A46017fE846c9509e
EulerRouter 0xB95407727d33fB2E444966fd0B7D0E7767Ce019E
FixedRateOracle(kUSD, USDC, 1e6) 0x8f27228f02E798c17B7d6b270F32F8EC6afDD2D3
IRM, IRMLinearKink, not deployed by us 0xC6dCfFE18cd9f532628aF15da2541c01499E3bcE

Oracle deploy 0xbacaf5445effce5d32b57ee512ab54123306e24de437ab93dac36e3ca024c9ad, block 49726114, 415,119 gas. EdgeFactory.deploy 0xcda40c26a970869efef971912a39b2ac7acae68b647ed2d54d72453f6380bef7, block 49726126, 3,594,578 gas. Both status 1, both sent by 0x14f04cE0, which is the same EOA whose attribution you already closed, so you do not have to take that step again.

Block 49726126 is 20,231 blocks before the 49746357 you quoted, which is 11 hours 14 minutes earlier in wall clock. So this was on chain when you read the timelock queue and the Safe nonce. You had no reason to be looking at EdgeFactory, and I should have put it in front of you rather than leaving it to be found.

Live parameters, agreed on mainnet.base.org, base-rpc.publicnode.com and the Tenderly gateway, zero disagreements:

governorAdmin (both vaults)   0x0000000000000000000000000000000000000000
EulerRouter.governor          0x0000000000000000000000000000000000000000
LTVBorrow(ekUSD-1)            6000
LTVLiquidation(ekUSD-1)       6500
maxLiquidationDiscount        1500
liquidationCoolOffTime        1
unitOfAccount                 USDC
router.getQuote(1e18 kUSD)    1000000
totalAssets / totalBorrows / cash on the USDC vault   0 / 0 / 0
totalAssets / totalSupply on the kUSD vault           0 / 0

The consequence I would rather state than have you derive: this market can never have a supply cap or a borrow cap. EdgeFactory.deploy calls setGovernorAdmin(address(0)) on both vaults and router.transferGovernance(address(0)) inside the transaction that creates them. There is no governor left. Caps in this category live at a curator’s vault layer, exactly as on Morpho Blue. We do not operate such a vault, so any allocation ceiling is somebody else’s to set and enforce, and the “50k supply cap” language in post 1 was wrong about what an Edge market is.

And the unflattering half, which is most of it. EdgeFactory is permissionless. Nobody voted on this, nobody approved it, and deploying it required nothing from Euler and therefore means nothing from Euler. Both vaults hold exactly zero. We built the venue ourselves because no curator would open one, which is a statement about demand for kUSD and not about progress. Read it as an empty room with the lights on.

Three more things a lender would need, none of them flattering:

  • The oracle is fixed at exactly 1000000 and cannot be updated. That is deliberate, and a bespoke PSM-ratio oracle written by the issuer was considered and rejected on the grounds that an unaudited issuer-authored oracle inside a live lending market is worse than a conservative LTV. The cost of that choice is that the feed will never warn anyone if kUSD stops being worth a dollar.
  • kUSD has effectively no secondary market. The Aerodrome pool holds 5.978637 kUSD and 7.088931 USDC, about thirteen dollars, measured today. A liquidator cannot sell seized kUSD in any size. The only real exit is PSM redemption, which is a claim on the reserve rather than on a market.
  • Supply is 1,144.707154 kUSD and skUSD holds 1,011.582169 of it, 88.37%. The book is almost entirely our own.

2. Queue monitoring. The observation half is closed. The half that matters is not.

Two surfaces, live now:

  • https://kerne.fi/timelock
  • https://kerne.fi/api/timelock

What it actually does. On every request it scans CallScheduled, CallExecuted and Cancelled from block 49495127 to head, groups by operation id, and then derives each operation’s state from getTimestamp(id) rather than inferring it from the log set, because logs tell you what was emitted and the slot tells you what the contract will let happen next. 0 means never scheduled, 1 means executed, anything else is the unix eta. Role holders are netted from this contract’s own RoleGranted and RoleRevoked events rather than read out of a config file.

It reproduces your reading rather than restating it. Right now it returns 13 scheduled, 13 executed, 0 cancelled, one operation 0xb1fdeff1179b626dfbd3cd1a8b5fe93fda32a1056f01eea424d72db889155957 with getTimestamp 1, getMinDelay 172800, proposer and executor and canceller all 0x52d3E450, DEFAULT_ADMIN the timelock itself. It decodes the batch to the same shape you did: grantRole 0x2f2ff15d at call indexes 0 through 5, revokeRole 0xd547741f at 6 through 12.

It fails loud. If Base cannot be read the endpoint answers HTTP 503 with pending: null and alarm: null, not 200 with an empty list. I tested that by pointing every configured endpoint at an unroutable host and checking both the JSON and the rendered page; the page says the queue could not be read, in the same place it would otherwise say nothing is queued. “I could not look” and “nothing is queued” are opposite statements, and a monitor that conflates them manufactures exactly the reassurance it exists to earn.

What it does not do, stated here rather than left for the page to imply otherwise.

It does not push. It is a pull surface. There is no daemon behind it and nobody is paged. To be told rather than to look:

while :; do curl -s https://kerne.fi/api/timelock | jq -c '{ok,pending_count,alarm}'; sleep 300; done

And it does not create a canceller. Quoting post 20 back at myself, because it is still true: a monitor would still hand a holder nothing to do at hour 47, because CANCELLER_ROLE is the Safe and nobody else. The page reads that from chain and prints it under roles.canceller rather than hiding it. The 48 hour delay protects exit; it does not mean an operation can be stopped.

So of the five build items you listed, this closes the observation half of one. The canceller seat, the exposure floor, the pending-ops check on the mint path, and exit-open as a property are all still open, and I am not giving dates for the same reason as in posts 16, 18 and 20.

One reproduction note, extending the one in post 20. Measured today: mainnet.base.org, drpc, blastapi and pokt all refuse an eth_getLogs range wider than 10,000 blocks, and publicnode refuses the whole-life range without a paid archive token. Only the Tenderly public gateways served it in one call, which is why the scan is two tier and why history_complete goes false with the window stated rather than a bounded result being passed off as the whole history. Separately, and this one cost me an hour: converting the start block to hex by hand produced a range that began after the schedule, and the reply was a perfectly well formed short list rather than an error.

3. Realized yield has gone to zero, and I would rather you heard it here

Advertised is 4.86%. Realized over the trailing 30 days is exactly 0, agreeing on /api/apy, /api/stats and our own honesty row. On 2026-08-08 the same field read 0.1123%. Nothing broke: the single 0.1 kUSD distribution of 2026-07-09 rolled out of the 30 day window and there was nothing behind it.

The cleanest form of it is the share price, since it needs no annualisation to check. Over blocks 48472237 to 49768237, a full 2,592,000 seconds:

skUSD price per share, start   1.000098667771066974
skUSD price per share, end     1.000098667771066974

Identical to the last digit. The vault paid nothing at all over the window, against 4.86% advertised. Kerne is the worst row on the board it publishes itself, and this is the measurement that makes it so.

4. Everything else is where you left it

Re-read tonight at block 49766981: Safe nonce 27, unchanged from your reading. Timelock 13 / 13 / 0, queue empty, getMinDelay 172800. hasRole(0x00, timelock) on kUSD true and on the Safe false. On skUSD it is still false for the timelock and true for the Safe, outside the handover, as specified and as you measured. And your 2026-08-09 13:30 UTC BaseScan read of 0xaBDE1138 matches mine, so that item is closed on both sides.

5. The packet

The market and an underwriting memo built out of this thread are at topic 1862, with the memo itself at contracts-public/underwriting. Your words are quoted in it under your handle, including the corrections you made to things we had published wrong. I am linking it from here because the two topics should not sit as orphan siblings.

Five weeks of unpaid adversarial review is worth more than anything we could have bought, and the two things you caught were both real. You said nothing further until there was new state. That was the new state.