Skip to content

Safety & owner model

Kairence is non-custodial smart contracts plus an off-chain backend for operational convenience. The custodial layer is the smart contracts — your DIEM, DD, PT20, PT721, and KAI all live on-chain.

This page documents what the protocol's owner can and cannot do, where the 7-day timelock applies, and what happens in failure modes.

Owner role

The deployer initially owns every admin-bearing contract. Pre-mainnet the owner is a single deployer EOA; before launch, ownership is transferred to a Gnosis Safe multisig via TransferOwnership.s.sol.

The owner can:

ActionConstraintRisk if compromised
Rotate Locker reference in Vault7-day timelock + cross-checkOld Locker breaks; user funds safe
Rotate UnstakeQueue reference in Vault7-day timelock + cross-checkOld queue breaks; user funds safe
Add/remove Locker in kDIEM allowlist7-day timelock + cross-checkNew Locker can mint kDIEM (and ultimately drain DIEM through PT20.deposit + maturity); 7d gives users time to exit
Add/remove Redeemer in kDIEM allowlist7-day timelock + cross-checkLimited — kDIEM.burn requires from == msg.sender, so a hostile redeemer can only burn its own balance
Add/remove Minter in DD allowlist7-day timelockNew minter can inflate DD; 7d gives users time to exit
Rotate Redeemer in Queue7-day timelock + cross-checkNew redeemer can request fake unstakes; 7d window for users to exit
Rotate Locker in DDMinter7-day timelockOld DDMinter stops being callable by new Locker
Rotate Locker in PT20Factory7-day timelockOnly affects PTs deployed AFTER rotation; old PTs unchanged
Rotate Locker in PT7217-day timelockNew Locker can mint new NFTs; old NFTs unaffected
Pause DD mintingInstantStops new locks from minting DD; transfers/burns unaffected
Pause LockerInstantStops new locks; existing positions unaffected
Pause RedeemerInstantStops new redeems; existing pending claims unaffected
Register / deregister a platform in DailyAuctionInstantAffects new bids only
Set backend address in DailyAuctionInstantCan change who calls markKeyIssued; doesn't affect bid funds
Set kaiPerDD[platform] (leftover KAI fee rate)Instant, capped at MAX_KAI_PER_DD = 1 KAI/DDOperational fee. Worst case: owner raises fee to the cap, making buyLeftover expensive; bid auction unaffected
armEmergency(recipient)7-day wait before executeEmergencyInitiateOwner can eventually drain all DIEM to recipient

The owner cannot:

  • Withdraw user PT20/PT721 balances directly.
  • Mint DD without going through DDMinter (which requires being on DD's allowlist).
  • Mint kDIEM without being on kDIEM's locker allowlist.
  • Force-claim someone else's queued DIEM.
  • Override an active timelocked proposal without going through cancel + re-propose (which restarts the 7-day clock).

The 7-day timelock pattern

Every wiring change uses the same shape:

solidity
// Day 0
owner.proposeXxx(newAddress);
// → emits Proposed event, records effectiveAt = now + 7 days

// Days 0..6
// Users see the event in indexers, can decide whether to exit.
// Owner can owner.cancelXxxChange() during this window.

// Day 7+
anyone.activateXxx();
// → applies the change, clears pending

This means:

  • A compromised owner cannot drain instantly. Worst case, they propose changes; you have 7 days to exit any way you can (DEX sell of PT20, redeem at maturity if applicable, etc.).
  • Activation is permissionless. Once the 7 days pass, anyone can call activate. If the owner is gone, the change still happens.
  • Cancellation is owner-only. Useful for "oops, wrong address" situations.

Wiring cross-checks

Every propose* and setXxxInitial does a sanity check against the target's own state:

  • Vault.setLockerInitial(L) requires Locker(L).vault() == address(this).
  • Vault.setUnstakeQueueInitial(Q) requires Queue(Q).vault() == address(this).
  • kDIEM.addLockerInitial(L) requires Locker(L).kDiem() == address(this).
  • kDIEM.addRedeemerInitial(R) requires Redeemer(R).kDiem() == address(this).
  • Queue.setRedeemerInitial(R) requires Redeemer(R).unstakeQueue() == address(this).
  • (Plus proposeXxx variants do the same check.)

Cross-checks do not protect against a malicious owner — they protect against deployment typos. A malicious owner can simply deploy a new contract with correct getters and propose it.

Emergency drain

Three-step procedure, total minimum elapsed time 7 days + Venice cooldown (≈1 day) = ~8 days:

1. owner.armEmergency(recipient)
   → records emergencyArmedAt = now
   → emits EmergencyArmed event with effectiveAt = now + 7 days

2. wait 7 days
   (users see the event, can exit through normal flows)

3. owner.executeEmergencyInitiate()
   → flips emergencyInitiated = true
   → blocks all normal flows (stake, initiateUnstake, claimUnstake, transferOut)
   → calls Venice initiateUnstake on the full staked balance

4. wait Venice cooldown (~1 day)

5. owner.executeEmergencyDrain()
   → calls Venice unstake (drains staked DIEM into Vault)
   → transfers FULL balance (staked-drained + any free DIEM) to recipient

cancelEmergency() is callable by owner during step 2 only.

This exists as a true last resort — e.g., a critical bug is discovered that puts user funds at risk and the protocol needs to be evacuated to a recovery address. It is owner-centralization by design; it is the strongest argument for the owner being a multisig with diverse signers.

Backend trust model

The backend operator (separate from contract owner) has exactly one on-chain power: auction.markKeyIssued(platform, day, bidIndex). This sets bid.keyIssuedAt so winners and indexers know the Venice API key was issued.

A compromised backend:

  • Can: Falsely mark keys as issued without actually issuing a Venice key. Winners burn DD/KAI without getting inference. This is operational loss for winners on the affected day.
  • Cannot: Drain anyone's DIEM, mint DD or KAI, override settlement, or affect refunds.

Mitigations:

  • HSM/KMS storage of the backend private key.
  • Owner can rotate the backend EOA instantly via setBackend(newBackend).
  • Monitoring + alerts on markKeyIssued events to detect inconsistencies.

Pause semantics summary

ContractWhat pause blocksWhat it does NOT block
DDmint()transfer, burn, approve
LockerlockPT20(), lockPT721()n/a
RedeemerredeemAndRequestUnstake(), redeemPT721AndRequestUnstake()n/a

Existing positions remain transferable and burnable during a pause — users can always exit even while issuance is frozen.

Defense-in-depth: ReentrancyGuard

All state-mutating external functions in Vault, Queue, and Auction are nonReentrant. DIEM and DD/KAI tokens don't have hooks today, but the guard closes a whole class of issues if a future integration ever introduces a token with callback behavior.

Audits and tests

  • Unit tests: 157 tests in test/unit/ + 22 in test/core/, all green.
  • Foundry invariants: 5 invariants × 128 000 random calls each — zero failures.
  • Echidna fuzz: 50 100 calls × 2 property checks — zero failures.
  • Slither static analysis: 45 findings, all false positives or by design.
  • Aderyn static analysis: 2 High + 7 Low, all false positives or by design.
  • External audit: Pending pre-mainnet engagement.
  • Bug bounty: Will be opened on Immunefi at mainnet launch.

See the contracts overview for per-contract notes.

Released under the MIT License.