Kerne Logo

Security / Safe queue

The Safe queue, decoded

Kerne is administered by a 2-of-3 Safe on Base. Its pending queue is public, and one of the entries in it is a fully signed transaction against the kUSD token contract that has been sitting there since April 2026. This page decodes every pending entry from its calldata, explains why that one can never execute, publishes how long the Safe has actually taken to produce a second signature, and recovers from chain which owner key signed which transaction, because two of the three owners have never sent a transaction in their lives and that looks worse than it is.

The queue rows and the latency table are a snapshot taken at block 49,631,013, 2026-08-06T21:22:53Z. Section 4b is different: it is read live from Base on every request, because who actually signs is the one claim on this page that must not be allowed to go stale. Every figure carries the command to re-read it. Nothing here needs our cooperation.

1. What is in the queue right now

Four entries. The Safe's nonce is 27 as last read, because the live read is failing, its threshold is 2, its guard slot is zero and its module list is empty. Three of the four target the governance timelock rather than a protocol contract, which is what the queue looks like after the 2026-08-06 custody handover: the Safe now asks the timelock for changes instead of making them directly.

Nonce 02 of 2 signaturessubmitted 2026-04-16T00:20:47Z

Target: kUSD token 0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3

selector 0x2f2ff15a, arguments MINTER_ROLE and 0x7286200Ba4C6Ed5041df55965c484a106F4716FD

Permanently unexecutable. Four independent reasons, section 2.

Nonce 271 of 2 signaturessubmitted 2026-08-04T17:15:03Z

Target: governance timelock 0x36A14976980B7Dd33136f6613545EB0A2C0a0D72

schedule(unpause() on 0xaBDE1138aa1Ce88d1dF06422C0c3b05D70569803, delay 172800)

Awaiting a second signature. Pre-arms the restart of the live mint module, section 3.

Nonce 281 of 2 signaturessubmitted 2026-08-04T17:15:05Z

Target: governance timelock 0x36A14976980B7Dd33136f6613545EB0A2C0a0D72

schedule(unpause() on 0x07eBb486e11BD217e6085eb5ab663e4517595993, delay 172800)

Awaiting a second signature. Pre-arms the restart of the retired module, section 3.

Nonce 291 of 2 signaturessubmitted 2026-08-04T17:15:06Z

Target: governance timelock 0x36A14976980B7Dd33136f6613545EB0A2C0a0D72

schedule(unpause() on 0xFf3025ec18e301855aB0f36Ec6ECa115a29A5Fbc, delay 172800)

Awaiting a second signature. Pre-arms the restart of the redeem reserve, section 3.

Every call above is operation 0, a plain CALL, with zero value and zero in every gas field. None of them is a DELEGATECALL, and none of them is wrapped in a MultiSend.

2. The fully signed entry at nonce 0, and why it is inert

This is the entry that looks alarming. It carries two confirmations out of two, it targets the kUSD token, and its payload contains the hash of MINTER_ROLE. A reader who stops there reasonably concludes that a mint-authority grant is one click from executing. It is not, and here is each reason separately, because any one of them is sufficient on its own.

  1. Its nonce is spent. Safe transactions consume nonces strictly in order, and the Safe's nonce is 27 as last read, because the live read is failing. A nonce-0 payload cannot be executed there: the nonce is part of the EIP-712 struct that the signatures are checked against, so the existing signatures do not even authorise a re-nonced version of the same call.
  2. Its function selector does not exist on kUSD. The payload begins 0x2f2ff15a. The selector for grantRole(bytes32,address) is 0x2f2ff15d. They differ in the last nibble. Searching the deployed kUSD runtime finds 0x2f2ff15d present and 0x2f2ff15a absent, which is an apples-to-apples control rather than a bare absence claim. The 4-byte directory has no entry for it at all.
  3. The identical payload has already been executed, and it reverted. On 2026-04-16 the Safe executed this exact call at nonce 0 in transaction 0x46eb8404 at block 44,762,366. The Safe emitted ExecutionFailure, not ExecutionSuccess. So this is not a theoretical argument about selectors: the call was tried on chain and did nothing. The pending entry is a duplicate proposal for the same nonce that was orphaned when the other one consumed it.
  4. Its target is a superseded contract that holds no role. The address in the payload, 0x7286200Ba4C6Ed5041df55965c484a106F4716FD, is the original April 2026 peg module, superseded twice since. Its own kUSD() getter still returns a placeholder address, so it was never wired to the live token, and hasRole returns false for it on both MINTER_ROLE and DEFAULT_ADMIN_ROLE today.

The honest summary is that an internal command line tool mis-encoded a function selector in April 2026, three transactions were executed with it and all three reverted, the bug was fixed, and the corrected call went through at nonce 3. The queue entry is the residue of that. It cannot be cancelled without spending a nonce on a replacement transaction, which is why it is still visible.

3. The three live entries, and what they do

All three are the same shape against a different module. Each one schedules an unpause() call through the governance timelock, and each was submitted on 2026-08-04 and is waiting on a second signature. They exist because of an asymmetry the custody handover created deliberately, and it is worth explaining rather than leaving to be discovered.

Why a pre-armed unpause is needed at all

After the 2026-08-06 handover, pause is instant and unpause is not. Pause runs through the PauseGuardian in a single Safe transaction. Unpause is gated on DEFAULT_ADMIN_ROLE, which the timelock now holds, so restarting a halted module takes the full 48 hours. That is the intended trade: it removes our ability to pause an exit and then quietly resume. The cost is that a module halted in an incident stays halted for two days.

These three transactions pay that cost in advance. Each schedules the unpause now, so its 48 hours elapse while nothing is wrong and the operation then sits ready indefinitely. They cannot fire while the system is healthy: unpause() reverts ExpectedPause() on a module that is not paused. A one-shot valve that only works when it is needed.

They are three separate operations rather than one batch on purpose. The timelock executes a batch atomically, so a single three-module batch would only fire if all three were paused at once, and would revert in the realistic case of the live mint module being halted on its own. One operation per module covers every combination. Only the Safe can execute them, because the executor role is not open to the zero address, so a pre-armed unpause cannot be turned against us.

What already executed, for the record

This page previously described nonces 19 and 20 as pending. Both executed on 2026-08-03. Nonce 19 revoked MINTER_ROLE from the v2 vault at 0x8ccc56B5624e2FDB592F6609d81F4c3798e3292B, which had held it since May 2026 and never used it, leaving the live mint module as the only holder. The vault could not have minted in any case: its deployed runtime contains no kUSD address, no mint selector and no delegatecall opcode. That was a clarity improvement rather than a security fix and we said so at the time. Nonce 20 widened the oracle staleness window on the mint module from 93,600 seconds to 172,800, the maximum the contract accepts. That was a deliberate loosening and it is recorded as one. In a genuine USDC depeg the staleness gate now reacts up to 48 hours late instead of 26. We judged the availability risk of a sub-two-hour margin over the feed's worst observed 24.008 hour heartbeat to be the larger exposure, given the depeg band is a separate check that was not touched. It is reversible by setting the value back, which now takes the timelock delay.

Nonces 21 through 26 were the custody handover itself, executed between 2026-08-03T23:24:07Z and 2026-08-06T17:00:07Z. Section 5 describes what they did.

4. How fast the Safe actually responds

Our anchor terms page has a section headed "who can change things, and how fast" and until this page existed it did not answer the second half. This is the answer, recomputed on 2026-08-24 across all 27 transactions the Safe has executed between 2026-04-16 to 2026-08-06. It is a description of past behaviour. It is not a service level, and we are not offering it as one.

IntervalMedianFastestSlowest
Proposal to second signature51m 35s43s25d 14h
Second signature to execution17m 07s2s2d 21h
  • 20 of 27 reached two signatures within 24 hours. 23 within 48 hours, 25 within 72 hours.
  • The tail is long and we are not hiding it. Two transactions proposed on 2026-05-17 waited 25 days and 14 hours for a second signature. Two more waited 2 days 19 hours, and another 1 day 5 hours. If you need a change made on a deadline, the median is not the number to plan against.
  • Three of the 27 executed with a failed inner call. Nonces 0, 1 and 2, all on 2026-04-16, all from the same command line tool and the same encoding bug described in section 2. The Safe transaction succeeded and the call inside it reverted. No transaction since April has failed.
  • The current queue is far slower than the median. The three timelock entries in section 1 were proposed on 2026-08-04 at 17:15 UTC and were still at one signature of two on 2026-08-24, which is longer than every wait in this record except the 25 day one. They are pre-arming operations rather than anything the protocol is waiting on, but a reader entitled to the median is entitled to this too.

4b. Which key signed which transaction

There is a cheap check on a 2-of-3 that looks damning, and anyone doing diligence on this protocol will run it. Read the owners off the Safe, then ask each of them for its transaction count:

cast call 0x52d3E450bA6c299B1B07298F1E87DD74732D4877 "getOwners()(address[])" --rpc-url $RPC
cast nonce <each owner> --rpc-url $RPC     # two of the three answer 0
cast balance <each owner> --rpc-url $RPC   # and hold nothing

Two of the three owners have never sent a transaction and hold no gas, and that is true on Base, Ethereum, Arbitrum, Optimism and Polygon alike. The natural reading is that the multisig is decorative and one person really holds everything. We would rather you find the answer here than assume that one.

A Safe signature is made off chain. An owner signs an EIP-712 digest with their key; a different party broadcasts the bundle and pays the gas. Signing is not sending, so a co-signer who has signed dozens of times still reports a transaction count of zero. The count that answers the question is not the owner's nonce, it is how many of the Safe's executed transactions carry that owner's signature, and that is sitting in the calldata of every one of them. Below, every signature is recovered from chain: the blob is pulled out of the Safe's own event, the EIP-712 digest is recomputed from the transaction fields and the nonce, and each signature is put through ecrecover.

The signature record could not be read right now. The full log history could not be fetched in one range, and this surface deliberately has no partial fallback: a short scan would undercount a co-signer and manufacture the impression it exists to refute. Nothing in this section should be read as either confirming or denying co-signer participation while that is the case. Recover it yourself with the commands in section 6, or read the Safe's events directly at BaseScan.

5. What a decoded queue does not fix

Nothing on this page reduces what two signatures can do. The thing that bounds them is the timelock, not the decoding. A decoded queue makes the position legible. It does not make it safer, and it should not be read as if it did. What changed the position is a separate event: on 2026-08-06 admin and manager rights on kUSD and the three PSM modules moved to a TimelockController with a 48 hour delay. Two signatures can still reach the reserve. They can no longer do it without two days of public notice, and they still cannot be stopped from pausing a module instantly.

There were two independent routes to that outcome and both are now behind the delay. The first is the one our terms page documents: DEFAULT_ADMIN_ROLE on kUSD can grant the minting role to any address. The second does not touch kUSD's admin role at all: the live mint module already holds MINTER_ROLE, and MANAGER_ROLE on that module gates listing a new collateral asset and pointing it at a price feed. We stated the second route explicitly because a remediation that closed only the first one would have looked like a fix and would not have been one.

The design that puts both routes behind a published delay, while keeping the emergency pause immediate, was written and tested against a fork of Base and executed on 2026-08-06 at 17:00:07Z, in transaction 0xed24d495 at block 49,623,130. The Safe no longer holds DEFAULT_ADMIN_ROLE on kUSD or on any of the three modules, and no longer holds MANAGER_ROLE on any of them. A TimelockController at 0x36A14976980B7Dd33136f6613545EB0A2C0a0D72 holds them, with a 48 hour minimum delay: the Safe is not that contract's admin, so two signatures cannot shorten it. A PauseGuardian at 0xC47adaf51907bB1871D07E18eA21dc75Ae93Cc8E holds MANAGER_ROLE for the single purpose of calling pause, so the emergency stop stayed instant. Fast to stop, slow to restart, deliberately.

Two limits worth stating in the same breath. The delay does not make a drain impossible. It makes it observable, and it gives a holder two days to leave through a redemption path that needs no permission from us. And it does not cover everything: the staking vault skUSD at 0x96F5102C15b839757f811A98CEc3725Ac21DfA14 holds most of the kUSD in existence and the Safe still holds sole admin over it with no delay at all. Extending the timelock to it is the next step and it has not been taken. The full position, including the fee and exit mechanics, is on the anchor terms page.

6. Reproduce all of it

These need no API key and no cooperation from us. Any public Base RPC works.

export RPC=https://base.gateway.tenderly.co
export SAFE=0x52d3E450bA6c299B1B07298F1E87DD74732D4877
export KUSD=0x5C2EfdF0D8D286959b42308966bc2B97f5680AA3

# The pending queue, straight from the Safe transaction service.
curl -s "https://api.safe.global/tx-service/base/api/v1/safes/$SAFE/multisig-transactions/?executed=false"

# The live nonce. Anything queued below this can never execute.
cast call $SAFE "nonce()(uint256)" --rpc-url $RPC

# No guard, no modules.
cast storage $SAFE $(cast keccak "guard_manager.guard.address") --rpc-url $RPC
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" \
  0x0000000000000000000000000000000000000001 10 --rpc-url $RPC

# Section 2, reason 2. The selector in the payload versus the real one.
cast sig "grantRole(bytes32,address)"        # 0x2f2ff15d, not 0x2f2ff15a

# Section 2, reason 3. The receipt that already reverted.
cast receipt 0x46eb84049ab234071d0a17220accb4e1f008a37ad2bb28b4b89f678d885eb6b8 \
  --rpc-url $RPC

# Section 2, reason 4. The target holds nothing.
cast call $KUSD "hasRole(bytes32,address)(bool)" \
  $(cast keccak "MINTER_ROLE") 0x7286200Ba4C6Ed5041df55965c484a106F4716FD --rpc-url $RPC

# Section 4. Every executed transaction, with confirmation timestamps. The
# timestamps are off chain, which is why the latency table is dated and 4b is not.
curl -s "https://api.safe.global/tx-service/base/api/v1/safes/$SAFE/multisig-transactions/?executed=true&limit=200"

# Section 4b, and none of it needs the Safe service. The L2 Safe emits every
# field of the transaction AND the raw signature blob AND a tail carrying the
# nonce, the executing sender and the threshold. Pull the whole life in one
# range; the common public providers cap eth_getLogs at 10,000 blocks and will
# refuse, which is why $RPC above is a Tenderly gateway.
cast logs --from-block 43683952 --address $SAFE \
  "SafeMultiSigTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes,bytes)" \
  --rpc-url $RPC

# Then, per event: recompute the SafeTx EIP-712 digest over its fields plus the
# nonce from additionalInfo, split the signatures blob into 65 byte words, and
# ecrecover each one. A word with v above 30 is an eth_sign signature: wrap the
# digest in the personal_sign envelope and subtract 4 from v first. v of 1 is a
# pre-approved hash and the owner address is in r. Cross-check any single digest
# against the contract, which will compute it for you:
cast call $SAFE "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)(bytes32)" \
  <to> <value> <data> <operation> <safeTxGas> <baseGas> <gasPrice> <gasToken> <refundReceiver> <nonce> \
  --rpc-url $RPC

# Section 4b. The owner set has never changed, so these three return nothing.
cast logs --from-block 43683952 --address $SAFE "AddedOwner(address)" --rpc-url $RPC
cast logs --from-block 43683952 --address $SAFE "RemovedOwner(address)" --rpc-url $RPC
cast logs --from-block 43683952 --address $SAFE "ChangedThreshold(uint256)" --rpc-url $RPC

# Section 5. The custody handover. DEFAULT_ADMIN_ROLE is bytes32(0), a run of
# 64 zeroes, NOT the keccak hash of the string. Returns false, then true, then
# 172800, then false again (the Safe is not the timelock's own admin).
ADMIN=0x0000000000000000000000000000000000000000000000000000000000000000
TL=0x36A14976980B7Dd33136f6613545EB0A2C0a0D72
cast call $KUSD "hasRole(bytes32,address)(bool)" $ADMIN $SAFE --rpc-url $RPC
cast call $KUSD "hasRole(bytes32,address)(bool)" $ADMIN $TL --rpc-url $RPC
cast call $TL "getMinDelay()(uint256)" --rpc-url $RPC
cast call $TL "hasRole(bytes32,address)(bool)" $ADMIN $SAFE --rpc-url $RPC

# Section 5. The limit of the claim, and the reason not to say "custody is
# timelocked". The staking vault holds most of the kUSD in existence and the
# Safe still administers it with no delay. Returns true.
cast call 0x96F5102C15b839757f811A98CEc3725Ac21DfA14   "hasRole(bytes32,address)(bool)" $ADMIN $SAFE --rpc-url $RPC

# Section 5. The handover receipt itself.
cast receipt 0xed24d495789540dae543793a5d8c9d884d8abfc386c0e726084da9183933d9b8   --rpc-url $RPC

# The minter set cannot be enumerated in one call: kUSD is plain AccessControl
# and getRoleMemberCount reverts. Replay the events instead.
cast logs --from-block 44448090 --address $KUSD \
  "RoleGranted(bytes32,address,address)" --rpc-url $RPC

The event replay needs chunked block ranges on every public provider we have tried. If a single call returns nothing, split the range rather than concluding there are no events.