Foundry fork of Base, pinned at block 50,400,000, 2026-08-24T16:35:47Z
One million dollars in, and back out again.
1,000,000 USDC into the live mint module, staked into skUSD, unstaked, and redeemed back to 998,800.35 USDC. Seven calls, one address that had never sent a transaction, against the bytecode that is deployed on Base today. The round trip costs 1,199.65 USDC, which is 11.9965 basis points.
Two things are true at once and the second one is why this page is worth reading.
This is fork state, not a mainnet transaction. Nothing here moved a real dollar. The mint leg is genuinely unconstrained: the cap is ten million and a deposit is not gated on the reserve. The redeem leg is funded solely by the module's own USDC reserve, and at this block the largest single redemption available anywhere in Kerne is 995.003000 USDC. The cycle below closes only because the mint immediately before it is what puts the reserve there. That is a statement about the mechanism at size. It is not a statement about liquidity, and it must never be quoted as one.
Eight tests, one command, and a signed artifact at /round-trip.json. You do not have to believe any of this. You can run it.
1. What actually ran
The depositor is 0xa72F3e0ddb8Fbdf4594cdc614528d95Ea46f2Eb9, derived from a string rather than picked, so you can regenerate it yourself with cast wallet address --private-key $(cast keccak "kerne round trip depositor"). At the pinned block it held no USDC, no kUSD, no skUSD, no ETH, no contract code and had a nonce of zero, and the test asserts every one of those. That matters because the claim being defended is that no allowlist, application or prior relationship is involved, and an address that already held something would not defend it.
| Leg | Call | Out | What happened |
|---|---|---|---|
Mint 1 of 4 | swapStableForKUSD(USDC, 1000000000000) | 999,500.00 kUSD | 1,000,000.00 USDC in. The fee is 5 bps at this size, so 500.00 USDC stays with the module and the exposure counter rises by the gross deposit. |
Stake 2 of 4 | skUSD.deposit(999500e18, depositor) | 999401391292319953981121958162 shares | The vault's assets rise by exactly the deposit. skUSD carries 24 decimals against an 18 decimal asset, which is why the share count looks the size it does, and it is the single most common way to integrate this token wrong. |
Unstake 3 of 4 | skUSD.redeem(shares, depositor, depositor) | 999,499.999999999999999999 kUSD | One wei short of what went in. ERC-4626 rounds shares down on deposit and assets down on redeem, so a same block in and out cannot be exactly lossless. The wei stays with the vault. |
Redeem 4 of 4 | swapKUSDForStable(USDC, 999499999999999999999999) | 998,800.35 USDC | The exit prices at 7 bps, not 5, because what came back from the mint is below the million dollar tier. 699.649999 kUSD of fee is retained as stable. |
Three approvals sit between those four calls, which is why the count is seven and not four. The whole cycle happens inside one block, so no yield vested and none was owed: skUSD streams distributed yield over a vesting period and excludes it from totalAssets() until it vests, which is the deliberate defence against exactly the flash deposit this shape would otherwise resemble.
2. Run it yourself
No key, no account, no permission, and no Kerne endpoint in the loop. A public Base RPC and Foundry are enough.
git clone --recurse-submodules https://github.com/kerne-protocol/contracts-public
cd contracts-public
BASE_RPC_URL=https://mainnet.base.org forge test --match-path 'test/fork/MillionDollarRoundTrip.t.sol' -vv8 tests, pinned to block 50,400,000 so the assertions are exact integers rather than approximations. The file is test/fork/MillionDollarRoundTrip.t.sol at commit 77dfa748c2.
Stated precisely so nobody is misled about what our CI does. Every file under test/fork is opt in: without BASE_RPC_URL it prints SKIPPED and passes, so a clean clone with no network never goes red. CI proves it skips cleanly. CI does not run the round trip. You run the round trip, and that is the entire reason this is published as a test rather than as a screenshot.
The assertions were mutation tested rather than assumed. Moving the returned figure by one wei, the exit ceiling by one wei, or the fee ledger under-count by one each break exactly one test and no others. That is what makes them a bound rather than a description: without it, every number here could be wrong in the same direction and the suite would still be green.
3. Why it costs twelve basis points and not ten
This is the arithmetic a reader is most likely to get wrong unaided, so it is asserted in the test rather than left as prose. The fee ladder on USDC charges 5 bps at or above 1,000,000, 7 at or above 250,000, 8 at or above 50,000, and 10 below that. A million dollars enters at 5 bps. What comes back is 999,500.00 kUSD, which is 500 short of the million dollar tier, so the exit prices one step up the ladder at 7 bps.
in 1,000,000.000000 USDC fee 5 bps 500.000000 USDC
out 998,800.350000 USDC fee 7 bps 699.649999 kUSD
cost 1,199.650000 USDC 11.9965 bpsSomebody assuming 5 bps each way gets 10 and is wrong by a fifth. Read the fee for your own size with getFee(USDC, amount) rather than doubling the entry fee. The redemption fee is charged in kUSD and retained as stable, which is why the cost lands at 11.9965 rather than a round 12.
4. Three things the run established that the arithmetic could not
The redeemed kUSD is burned, not parked
kUSD supply before the trip was 1,144.707154. After it, it was 1,144.707154000000000001, and the module held 0 kUSD. The whole 999,500 was destroyed on the way out. That is a 2026-07-06 fix carried by the deployed build: without it, returned kUSD would sit in the module as dead inventory while the next mint minted afresh, so a caller paying only fees could inflate reported supply without bound. A complete round trip therefore nets to fees only, and the difference of a single wei is the staking rounding described below rather than a leak.
The fee ledger under-counts by exactly one stable-wei
The module keeps 1,199.650000 USDC. The internal ledger that bounds how much of that can ever be skimmed records 1,199.649999, one millionth of a dollar less. That is a documented flooring in the redeem path, observed at size here for the first time rather than reasoned about, and the direction is the safe one: the ledger can never authorise a skim into backing. It is published because a reviewer reconciling the two numbers will find the gap, and finding it explained is different from finding it.
The staking leg returns one wei less than it took
999,500 kUSD in, 999,499.999999999999999999 back. ERC-4626 rounds shares down on deposit and assets down on redeem, so a same block in and out cannot be exactly lossless, and the wei accrues to whoever is still staked. Kerne could round this away in the copy and say the tidier thing. The tidier thing is false by one wei, and a page that rounds a wei is a page that will round something larger later.
5. What the exit actually is today
Everything above is the mechanism. This is the constraint, and it is asserted in the same test file rather than written here and hoped for.
Present the live module with 999,500 kUSD and no preceding mint and it reverts InsufficientStableReserves() 0x49d5a855. The identical call succeeds in the round trip above, and the entire difference is that the mint one line earlier funded the reserve. Redemptions are paid out of the module's own USDC and nothing else: an earlier design drew on the insurance fund on a shortfall, and that branch was removed in June 2026 because the fund's asset is WETH and the draw asked for USDC, so it could never have worked.
| Module | Largest single exit | What it is |
|---|---|---|
Live mint module 0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803 | 30.000000 USDC | The only module that can mint. Its reserve is what a depositor's own exit draws on. |
Retired mint module 0x07eBb486e11BD217e6085eb5ab663e4517595993 | 995.003000 USDC | Minting role revoked. Retained as a redeem reserve, and today it is the deepest of the three. |
v1 redeem reserve 0xFf3025ec18e301855aB0f36Ec6ECa115a29A5Fbc | 85.885006 USDC | Minting role revoked. Retained as a redeem reserve while reserves migrate. |
The three reserves do not pool. 1,110.888006 USDC exists across them, and no single call can reach it, so the honest ceiling to quote is the deepest module and not the sum. The test walks that ceiling to its edge the way the capacity test walks the mint: a redemption of 995.998999999999999998 kUSD pays out the deepest module's entire reserve and empties it, and one wei more reverts.
So the number a reader may have seen elsewhere, that a holder can take out roughly $995 in one transaction, is correct, and it belongs to a holder who did not mint. It is not a property of the contract. It is the depth of one module on one day, and it moves the moment anybody deposits. What this page adds is that the ceiling is a function of reserve depth rather than a limit written into the code, and it adds it by executing both sides rather than by asserting it. The reserve as it stands right now, rather than at the pinned block, is on /first-money, which recomputes every figure live.
6. The artifact, and what signing it does and does not mean
Every figure on this page is served as a signed document at /round-trip.json. The digest is a sha256 over canonical JSON and the signature is an EIP-191 personal sign over that digest, which is the same scheme and the same signer account as the Proof of Reserves archive, so one address and one command cover both.
signer 0x09a2780ac8Be6D5d2d1F85A8D92b09D40C9CA37e
digest 0xda897374f22442efe7fcd861c3b8ead906bd581bc73824fe897ab26a30669d5f
signature 0x50f21696cf3f2210a60d4ef4c92b24c7e20447373430ffc4b329be1d3da93388068942f135e89f0e0ebc2b262fa83326632d07275a5cc7d782f5ae395d36a98b1c
python - <<'EOF'
import json, hashlib, urllib.request
from eth_account import Account
from eth_account.messages import encode_defunct
d = json.load(urllib.request.urlopen("https://kerne.fi/round-trip.json"))
h = "0x" + hashlib.sha256(d["signed_payload_canonical"].encode()).hexdigest()
assert h == d["attestation_hash"], "digest does not cover the payload"
print(Account.recover_message(encode_defunct(hexstr=h), signature=d["signature"]))
EOFWhat the signature does not mean. The signer is Kerne. It is not a third party and this is not an attestation by anybody else, so the signature proves only that this document is the one Kerne published and has not been altered since. The part that does not require trusting Kerne is the test, which runs against bytecode Kerne cannot change from a repository anybody can clone. If those two ever disagree, the test is right.
This is not the first time the cycle was run, and saying so would be false
A mint and redeem round trip was executed on a Base fork on August 3, 2026 at block 49,493,576 and returned the same 998,800.350000 USDC for the same 1,199.65 USDC of cost. It lived in a source comment on the mint page, was never published, never signed, had no staking leg and asserted no boundary. It is named here for two reasons: because a claim of being first would be untrue, and because two runs three weeks and roughly nine hundred thousand blocks apart agreeing to the cent is better evidence than a first would have been.
The mint side of this, including the cap, the revert one USDC past the published headroom, and the disclosures that belong beside a capacity figure, is at /dossier/capacity, proved by test/fork/PsmCapacityIsReal.t.sol. What a deposit of this size would mean next to everything Kerne currently holds, which is the question a reader should ask immediately after this one, is on /first-money.