External audit
The Hexens audit of Kerne, in full
Final report published July 31, 2026. Page last updated August 24, 2026.
Hexens reviewed kUSD, skUSD, KUSDPSM, KerneVault, and esKERNE, each pinned to commit 0912c870, and published its final report on July 31, 2026. It found 10 issues: none critical, 2 high, 2 medium, 4 low and 2 informational. 8 were fixed and 2 were acknowledged without a code change. All 10 are in KerneVault.
Read that with the next sentence attached, because it is the one that matters. An audit reviews a commit, not a chain. The live KerneVault was deployed on June 16, 2026 from source older than the reviewed commit, so every vault finding below is open on the bytecode running today. That vault holds no user funds, has never issued a share, and deposits into it are closed on chain until the remediated build is deployed and verified.
What the document is
| Reviewer | Hexens, review led by Trung Dinh, Lead Security Researcher. Kerne paid for the engagement, which is true of essentially every audit in this industry and is rarely said out loud. |
|---|---|
| Report | hexens-kerne-protocol-final-2026-07-31-updated-2026-08-20.pdf (29 pages, 10,184,151 bytes), committed verbatim and unedited in the public contract registry. Hexens also publishes it on their own domain, which is the copy to read if you would rather not take a protocol's word for a report about itself, and Kerne appears on their public client index with the same finding counts, so the tally in the table below can be read off a domain Kerne cannot write to. Ours stays up because it is content-addressed by the hash below. |
| SHA-256 | 1f858d9105c74e845f59db208ffbb0fd1e21cbbf07336347c5b09b5078fb7298 |
| Superseded copy | hexens-kerne-protocol-final-2026-07-31.pdf (29 pages, 8,269,537 bytes), SHA-256 655e7126030c750e9d58f2ab30b58215ce604942f06997d5c01532d6f687a4ca. The copy published on July 31, 2026, left exactly where it was so the hash published that day still verifies the bytes it pointed at. |
| Commit reviewed | 0912c870a89f1fa707f69c60fc05c05ea85e2fa8, frozen before fieldwork began so the reviewed code could not move underneath the reviewers mid engagement. |
| Scope | Five contracts: kUSD, skUSD, KUSDPSM, KerneVault, and esKERNE. The report names each one at the frozen commit. |
| Remediation commit | 98f29e55, named in the report's own scope section. |
| Timeline | Fieldwork started July 13, 2026. Initial report July 20, 2026. Revision received July 21, 2026. Final report July 31, 2026. |
The hash is there so you do not have to trust the filename. Download the PDF, hash it, and confirm you are reading the same bytes Kerne published. The whole audits directory, including the earlier independent researcher review and the deployed-versus-source map, is at contracts-public/audits.
One note on the report's own scope links. In the copy published on July 31, 2026 they pointed at the private development repository, so all five of them, and the remediation link beside them, returned 404 for any reader. That was our problem rather than the auditor's. Hexens reissued the report on August 20, 2026 with that section repointed into this registry, and the linked copy above is the reissued one, so those links resolve. The superseded copy still carries the dead ones, which is part of why it is still listed rather than swapped out underneath anyone. Independently of the report, the five reviewed files are published verbatim at contracts-public/audits/scope, alongside the same five at the remediation commit, each with its SHA-256. Diffing the two directories reproduces the dispositions in the table below from code rather than from our description of it: only KerneVault.sol changes, in 12 hunks, and the other four files are byte-identical at both commits.
What it found
10 findings in total. 8 fixed, 2 acknowledged without a code change. On the re-test the report records that all identified issues were either remediated or acknowledged by the development team and subsequently verified by its auditors.
Every one of the 10 is in KerneVault, the contract that holds assets and does the accounting. kUSD, skUSD, the peg stability module and esKERNE drew none between them. A review finding nothing in a contract is evidence, not proof: the honest reading of a zero is that a competent team looked and did not find something. One further caveat we would want made if this were someone else's page: both high findings concern the escrow token's forfeiture mechanism, and the report files them against the vault because that is where the offending code sits. Zero findings filed against esKERNE is not the same as the escrow design going unexamined.
Every finding
Ids, titles, severities and dispositions are the report's own. Each row links to our response below, and each response has a stable anchor so a single finding can be linked on its own.
| ID | Title | Severity | Disposition |
|---|---|---|---|
| KERNE1-4 | Transferable kLP shares can separate withdrawal and esKERNE forfeiture identities | High | Acknowledged |
| KERNE1-5 | esKERNE forfeiture can be bypassed by gas-starving the best-effort forfeit call | High | Fixed |
| KERNE1-7 | totalAssets silently under-reports NAV when the verification node call fails | Medium | Fixed |
| KERNE1-9 | _checkCRCircuitBreaker should be applied to captureFounderWealth | Medium | Fixed |
| KERNE1-2 | Unable to update offChainAssets when offChainAssets == 0 and _offChainAssetsBootstrapped == true | Low | Fixed |
| KERNE1-3 | maxTotalAssets check overestimates deposited assets | Low | Fixed |
| KERNE1-10 | _checkCRCircuitBreaker should pause the protocol when the collateral ratio exceeds SAFE_CR_THRESHOLD | Low | Acknowledged |
| KERNE1-11 | Withdrawal Requests Are Not Bound to the Share Price at Request Time | Low | Fixed |
| KERNE1-6 | Redundant _checkSolvency() function in KerneVault | Informational | Fixed |
| KERNE1-8 | Redundant extracted variable in captureFounderWealth function | Informational | Fixed |
Acknowledged means Kerne agreed the finding is valid and did not ship a code change. It is listed here with the same weight as the 8 that were fixed, and the report carries Kerne's written reasoning on both.
Where these findings land on the live system
The sentence at the top of this page, that every vault finding is open on the bytecode running today, is the one we lead with because it is the one that sounds worst. On its own it is also incomplete. This section is the rest of it. Every number below was read off Base at block 49,493,884 and each one comes with the call that produces it, so none of it has to be taken on our word.
The contract holding all 10 findings
KerneVault v2, 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B. Four things are true of it on chain right now.
| Can it accept a deposit? | No. maxDeposit returns 0 for any address and whitelistEnabled is true. Public deposits were closed on chain on July 30, 2026. |
|---|---|
| Has it ever issued a share? | No. totalSupply is 0, and it has been 0 for the whole life of the contract. No third party has ever held a kLP share, so there is no depositor for these findings to have harmed. |
| Can it mint kUSD? | No, and this changed today. Its kUSD MINTER_ROLE was revoked by the 2-of-3 Safe at block 49,470,172, 04:01:31 UTC on August 3, 2026, in this transaction. We should be precise about what that is worth: the vault's deployed runtime contains no kUSD address and no mint selector, so it could never actually have minted. Revoking the role removed a capability that was granted and never usable, and it means the minter list no longer needs a footnote. |
| Can anyone still get out? | Yes. paused is false, deliberately, so withdrawals are untouched. There is nobody to withdraw, which is the previous row, but closing the door on exits to look safer would have been the wrong trade. |
The contract money actually moves through
Minting kUSD today happens in the peg stability module, 0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803. Since the revocation above it is the only contract holding kUSD MINTER_ROLE. It drew zero findings in this audit.
There is a stronger fact available about it than that, and it is the one worth checking. The bytecode on that address is compiled from exactly the source Hexens reviewed. Sourcify reports creationMatch and runtimeMatch both exact_match for it, and the KUSDPSM.sol in that verified bundle is byte-identical to the file at the reviewed commit. Both hash to 013bd7e6fc2bcb5e780aaa5f924a905f3f63e88bee49d1bf3d832d7c963b99f9. So on the live mint path, reviewed and deployed are the same code, which is the thing the vault row above cannot say.
What this does not mean
It does not make the vault findings moot. They are real, two of them are High, and they stay open until a remediated build is deployed and verified. It also is not a claim that the deployed system as a whole runs reviewed code, because it does not. The live kUSD token, to take the clearest example, still exposes an ungated burnFrom that current source puts behind a role. That gap and the two others are set out with addresses and operating rules at /security/deployed-vs-source.
The narrow claim, and the only one we make: all 10 findings sit in a contract that cannot take a deposit, has never issued a share and can no longer mint, while the contract on the live mint path drew none of them and runs the reviewed source. One caveat on the word only. kUSD is plain AccessControl, not the enumerable variant, so proving a role set has exactly one member means replaying RoleGranted and RoleRevoked from the token's creation block rather than reading a counter. And it is one minter today, not one minter forever:DEFAULT_ADMIN can still grant the role again. Since 2026-08-06 that role is held by a 48 hour timelock rather than by the Safe, so a new minter has to be scheduled in public and sits visible for two days before it lands.
Check it yourself
RPC=https://mainnet.base.org
MINTER=0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6
# The vault carrying all 10 findings: shut, empty, and no longer a minter.
cast call 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B "maxDeposit(address)(uint256)" \
0x0000000000000000000000000000000000000001 --rpc-url $RPC # 0
cast call 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B "totalSupply()(uint256)" --rpc-url $RPC # 0
cast call 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 \
"hasRole(bytes32,address)(bool)" $MINTER 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B --rpc-url $RPC # false
# The live mint path, which drew no findings.
cast call 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3 \
"hasRole(bytes32,address)(bool)" $MINTER 0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803 --rpc-url $RPC # true
curl -s https://sourcify.dev/server/v2/contract/8453/0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803 # exact_match
# Its reviewed source, byte for byte.
curl -sL https://raw.githubusercontent.com/kerne-protocol/contracts-public/main/audits/scope/0912c870/src/KUSDPSM.sol \
| sha256sum # 013bd7e6fc2bcb5e...Our response, finding by finding
Written by Kerne, not by the auditor. The full narrative version, including the correction we sent the auditor about our own first answer, is in the response post. Where this page summarises a finding in plain language, the report's own text governs.
Transferable kLP shares can separate withdrawal and esKERNE forfeiture identities
Acknowledged rather than patched, and the reasoning is the part worth reading. The obvious fix is to make vault shares non transferable while the forfeiture mechanism is set. We implemented that, and our own adversarial re-review caught what it does: those shares are the collateral asset for kUSD borrowing positions, so blocking wallet to wallet transfers would have broken collateral deposits and made underwater positions impossible to liquidate, because a liquidator seizing collateral is itself a share transfer. The second obvious fix, firing forfeiture whenever shares move, would slash a user who merely posted shares as collateral. A correct fix has to tell an economic exit apart from a collateralization or liquidation movement, which is a design decision rather than a patch. What makes the wait acceptable: the forfeiture mechanism is inert on chain today, nothing has ever been emitted for it to act on, and the token it would redeem into is retired. It will not be enabled until the ownership versus forfeiture binding is designed and re-tested.
esKERNE forfeiture can be bypassed by gas-starving the best-effort forfeit call
Fixed with a gas floor. Before the forfeiture call the contract now requires a minimum amount of gas remaining, and reverts the whole withdrawal if it is not there. The outcome set goes from three possibilities to two: either forfeiture runs, or the withdrawal fails. The quiet third option, where the withdrawal succeeds and the penalty is skipped, is gone. Ordinary full gas withdrawals are unaffected.
totalAssets silently under-reports NAV when the verification node call fails
Fixed. When the verification node call failed, the vault's total assets reading silently dropped the off-chain buckets instead of falling back to the full formula, so a transient outage briefly made the vault look poorer than it was, and anyone who could trigger that outage on demand had a cheap mint and redeem window. The fallback path now returns the complete figure.
_checkCRCircuitBreaker should be applied to captureFounderWealth
Fixed. The function that captures founder revenue reduced assets without arming the collateral ratio circuit breaker, unlike the three other paths that reduce assets. It arms it now.
Unable to update offChainAssets when offChainAssets == 0 and _offChainAssetsBootstrapped == true
Fixed, and this one was our fault in a specific way. In May 2026 we hardened the vault's rate limiting so a strategist could not zero a bucket and then restore it to bypass the per update change cap. That hardening introduced this bug: once a bucket legitimately sat at zero, the rate limit maths made every non-zero write revert forever, with no path back up. We did not find it, Hexens did. The fix replaces the silent failure with an explicit error that names the recovery path, a deliberate multisig gated re-seed rather than something the strategist can do alone, so the original bypass stays closed.
maxTotalAssets check overestimates deposited assets
Fixed. The deposit cap counted the deposit fee as though it stayed in the vault, so deposits were turned away slightly early. The check now excludes the fee.
_checkCRCircuitBreaker should pause the protocol when the collateral ratio exceeds SAFE_CR_THRESHOLD
Acknowledged rather than patched. The recommendation was to pause the vault when the collateral ratio rises above the safe threshold, on the theory that an inflated ratio signals manipulation. A healthy delta neutral vault routinely runs above that threshold in ordinary operation, so a hard pause there would halt the vault on good days rather than only on bad ones, and picking a sensibly high bound is a tuning question we would rather resolve deliberately. In the meantime the bucket updates that could inflate the ratio are rate limited and cooldown gated, withdrawals route through a request queue and can never be same block with an inflation, and the KERNE1-11 fix caps the payout at the request time snapshot, which removes the profit from the scenario the finding describes. We told the auditor we are open to adding a conservative upper bound alert as a follow up.
Withdrawal Requests Are Not Bound to the Share Price at Request Time
Fixed. A withdrawal claim is now bound to the share price at the moment the withdrawal was requested, rather than settling at whatever the price is when the claim lands.
Redundant _checkSolvency() function in KerneVault
Fixed in the remediation commit. An internal function that nothing called.
Redundant extracted variable in captureFounderWealth function
Fixed in the remediation commit. A local variable that duplicated a value already in hand.
What this report does not say
Read this part before you read the table above.
- An audit reviews a commit, not a chain. The reviewed commit is 0912c870. The remediation commit is 98f29e55. Neither is the bytecode running in the live KerneVault, which was deployed on June 16, 2026 from earlier source. The vault findings above are open against it. The full map, with commands to check every claim yourself, is at /security/deployed-vs-source.
- A completed audit is not a guarantee of safety. We will not describe this code as passed, clean, secure or safe, now or after any future audit. The report does not use those words either.
- The off-chain hedge leg was not in scope. It runs on a single venue and is self reported rather than independently re-derivable on chain, and a smart contract review cannot attest it. Neither our operational security nor our key custody practice beyond what the contracts encode was in scope.
- 2 findings were acknowledged, not fixed. They are listed above with the same weight as the 8 that were fixed.
Everything else we disclose is exactly where it was before this report. The open gaps we know about are listed at /legible, the audit diary and the earlier internal adversarial reviews are at /security/audits, and the bug bounty and findings-response protocol are unchanged at /security.
Finding counts, severities and dispositions are as stated in the published Hexens report. Nothing here is investment advice and nothing here is an offer. An external audit is a review of specific contracts at a specific commit and is not a guarantee of safety, a solvency opinion, or a statement about the off-chain leg of the design.