KIP 290: Permissionless Smart Contracts Source

AuthorLewis, Ian, Ollie, Lake, and Joseph
Discussions-Tohttps://github.com/kaiachain/kips/issues/90
StatusDraft
TypeCore
Created2026-01-23
Requires 81, 226, 227, 286, 287
Replaces 113, 277

Abstract

This standard defines the spec for core smart contracts required to support permissionless validator operations. The contracts with major changes include AddressBookV2, CnStakingV4, PublicDelegationV2, and VRank. Other system contracts may have minor changes to accommodate these updates. These contracts replace or extend existing contracts while maintaining backward compatibility where possible.

Motivation

The updated contracts introduce the following improvements:

  • Flexible key management: Validators can use external multisig solutions (e.g., KaiaSafe) or EOAs, instead of relying on embedded multisig functionality.
  • Simplified data model: Each validator is represented by a single (nodeId, stakingContract, rewardAddress) entry, eliminating the complexity of consolidated staking.
  • Unified validator registry: BLS public keys are stored directly in AddressBookV2, providing a single source of truth for all validator information.
  • Upgradeability: Some contracts are deployed as upgradeable proxies, enabling protocol improvements without requiring validator migration.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Parameters

Constant Value
FORK_BLOCK TBD

Overview

The following contracts are introduced or updated:

Contract Description
AddressBookV2 Unified validator registry with BLS keys and state management
CnStakingV4 Staking contract for validators
PublicDelegationV2 Instant delegation with transferable tokens
VRank Validator ranking system

SimpleBlsRegistry (KIP-113) is obsoleted by AddressBookV2.

Some contracts (e.g., VotingV2, StakingTrackerV3, CLRegistryV2, AuctionFeeVault) might have minor changes to accommodate the above updates.

AddressBookV2

AddressBookV2 serves as the unified registry for all validator and candidate information. It consolidates functionality previously spread across AddressBook, CnStaking, and SimpleBlsRegistry (KIP-113).

Key Changes from AddressBookV1

  • Integrated information: On-chain information such as BLS key or gcId shall be stored directly in AddressBookV2.
  • Unique reward address: Each validator entry must be a unique combination of (nodeId, stakingContract, rewardAddress). Stake consolidation by reward address shall no longer be supported, as mentioned in KIP-287.
  • Removed embedded multisig: Administrative operations shall not use built-in multisig.
  • State management: Validator states as defined in KIP-286 shall be stored and managed within AddressBookV2.
  • Backward compatible interface: The contract shall implement the existing AddressBook interface to maintain compatibility with nodes and other contracts.
  • Only CnStakingV4: The new address book must only accept CnStakingV4 deployed properly by the factory.
  • Upgradeable: The contract must be deployed as an upgradeable proxy to enable future improvements.

Data Structures

struct NodeInfo {
    /// @dev Manager address for updating modifiable fields
    address manager;
    /// @dev Associated staking contract address (immutable)
    address stakingContract;
    /// @dev Address to receive block rewards (immutable when PublicDelegation is enabled)
    address rewardAddress;
    /// @dev Address for on-chain governance voting (mutable)
    address voterAddress;
    /// @dev Pause or idle timeout timestamp, 0 if not paused or idle
    uint256 timeoutAt;
    /// @dev Governance Council ID, only for GCs (immutable)
    uint256 gcId;
    /// @dev BLS public key and proof-of-possession (immutable)
    BlsPublicKeyInfo blsInfo;
    /// @dev Validator metadata in JSON format (mutable)
    string metadata;
    /// @dev Current state of the node
    State state;
}

/// @notice Lightweight struct for getAllProfiles() — no BLS data
struct Profile {
    address nodeId;
    address stakingContract;
    address rewardAddress;
    uint256 timeoutAt;
    State state;
}

enum State {
  Unknown,      // 0
  Registered,   // 1
  CandReady,    // 2
  CandTesting,  // 3
  ValInactive,  // 4
  ValReady,     // 5
  ValActive,    // 6
  ValPaused,    // 7
  ValExiting    // 8
}

Note: nodeId is used as the mapping key in AddressBookV2 and is not stored inside the NodeInfo struct. The timeoutAt field records the expiry timestamp for ValPaused (pause timeout) and ValInactive/ValReady (idle timeout) states; it is 0 when no timeout is active.

Node Metadata Schema

The metadata field in NodeInfo is a JSON-encoded string. All fields are optional. Unrecognized fields SHOULD be ignored by readers.

Field Type Description
name string Human-readable node operator name
thumbnail string URL to operator thumbnail/logo
summary string Short description
description string Long description
categories string[] Categorization labels
websites string[] URLs associated with the operator
communities string[] Community/social links

Example:

{
    "name": "Kaia Foundation",
    "thumbnail": "https://cdn.prod.website-files.com/66a8ba5239a3fbe8e678da2a/69866193d03bf54b81aa0283_66ce395e60f880698890073dcc91fa28_kaia-logo.svg",
    "summary": "The organization behind the Kaia blockchain.",
    "description": "The Kaia Foundation is the organization behind the Kaia blockchain, an EVM-compatible Layer 1 built for stablecoin settlement and onchain finance across Asia.",
    "categories": ["foundation", "layer1", "asia"],
    "websites": ["https://kaia.io", "https://docs.kaia.io", "https://portal.kaia.io"],
    "communities": ["https://x.com/KaiaChain", "https://blog.kaia.io"]
}

Interface

AddressBookV2 replaces the existing AddressBook at 0x0000000000000000000000000000000000000400 starting from FORK_BLOCK. The interface is divided into user functions (called by node manager), system functions (called by core client via system transaction following EIP-4788 convention), and admin functions (called by contract owner).

pragma solidity ^0.8.0;

import {State, BlsPublicKeyInfo, NodeInfo, Profile} from "./types/Node.sol";

interface IAddressBookV2 {
    // ── Events ──
    event StateChanged(address indexed nodeId, State indexed fromState, State indexed toState);
    event NodeCreated(address indexed nodeId, uint256 gcId);
    event NodeDeleted(address indexed nodeId);
    event SystemTransitionProcessed(address[] nodeIds, State[] newStates);
    event EpochTransitionProcessed(uint256 epochVACount);

    // ── User Functions (called by node manager) ──

    /// @notice Register a new node in Registered state
    function createNode(
        address nodeId, address stakingContract, address rewardAddress,
        address voterAddress, BlsPublicKeyInfo memory blsInfo, string memory metadata
    ) external;

    /// @notice Remove a Registered node entirely
    function deleteNode(address nodeId) external;

    /// @notice Registered → CandReady
    function readyCandidate(address nodeId) external;

    /// @notice CandReady → Registered
    function unreadyCandidate(address nodeId) external;

    /// @notice ValInactive → ValReady
    function readyValidator(address nodeId) external;

    /// @notice ValReady → ValInactive
    function unreadyValidator(address nodeId) external;

    /// @notice ValActive → ValPaused
    function pause(address nodeId) external;

    /// @notice ValPaused → ValActive
    function resume(address nodeId) external;

    /// @notice ValActive/ValPaused → ValExiting
    function exit(address nodeId) external;

    /// @notice ValInactive → Registered
    function offboard(address nodeId) external;

    function updateManager(address nodeId, address newManager) external;
    function updateRewardAddress(address nodeId, address newRewardAddress) external;
    function updateVoterAddress(address nodeId, address newVoterAddress) external;
    function updateMetadata(address nodeId, string calldata newMetadata) external;

    // ── System Functions (called by core client) ──

    /// @notice Batch state transitions computed by the core client
    function processSystemTransition(
        address[] calldata nodeIds, State[] calldata newStates, uint256[] calldata timeoutAts,
        uint256 epochVACount
    ) external;

    // ── Admin Functions (called by contract owner) ──

    function suspendValidator(address nodeId) external;
    function unsuspendValidator(address nodeId) external;
    function updatePauseTimeout(uint256 newPauseTimeout) external;
    function updateIdleTimeout(uint256 newIdleTimeout) external;
    function updateMaxNodeCount(uint256 newMaxNodeCount) external;
    function updateMaxValActivePausedCount(uint256 newMaxValActivePausedCount) external;
    function updateMaxCandReadyCount(uint256 newMaxCandReadyCount) external;
    function updatePfsThreshold(uint256 newPfsThreshold) external;
    function updateCfsThreshold(uint256 newCfsThreshold) external;

    // ── Getters ──

    function getNodeInfo(address nodeId) external view returns (NodeInfo memory);
    function getNodeInfos(address[] calldata nodeIds) external view returns (NodeInfo[] memory);
    function getAllProfiles() external view returns (Profile[] memory);
    function getAllBlsInfo() external view returns (address[] memory, BlsPublicKeyInfo[] memory);
    function getNodeState(address nodeId) external view returns (State);
    function getStateCount(State state) external view returns (uint256);
    function getEpochVACount() external view returns (uint256);
    function getSlotLimits() external view returns (uint256 maxSlot, uint256 minActive);
    function getSlotLimitsFor(uint256 n) external pure returns (uint256 maxSlot, uint256 minActive);
    function getRegisteredNodes() external view returns (address[] memory);
    function getSuspendedValidators() external view returns (address[] memory);
    function epochBlockInterval() external view returns (uint256);
    function currentEpoch() external view returns (uint256);
    function getTimeouts() external view returns (uint256 pauseTimeout, uint256 idleTimeout);
    function getMaxCounts() external view returns (uint256 maxNodeCount, uint256 maxCandReadyCount);
    function getMaxValActivePausedCount() external view returns (uint256);
    function getPfsThreshold() external view returns (uint256);
    function getCfsThreshold() external view returns (uint256);
}

CnStakingV4

CnStakingV4 is the staking contract used by candidates and validators to stake KAIA and manage their node operations.

Key Changes from CnStakingV3

  • Removed embedded multisig: The contract does not include built-in multisig functionality. Validators MAY deploy the contract with a multisig wallet (e.g., KaiaSafe) as the owner, or use an EOA.
  • No initial lockup: The initial lockup mechanism, originally designed for token lockup of initial investors before network launch, shall be removed.
  • Removed redundant information: Information stored in CnStakingV3 (such as rewardAddress, stakingTracker, voterAddress, gcId) shall be managed by AddressBookV2.
  • Upgradeable: The contract must be deployed as a beacon proxy, allowing the protocol to upgrade all CnStakingV4 instances simultaneously.
  • Factory deployment: A dedicated factory contract must handle CnStakingV4 deployment, ensuring consistent initialization and registration with AddressBookV2. The factory uses CREATE2 for deterministic proxy addresses.

CnStakingV4 Interface

pragma solidity 0.8.25;

interface ICnStaking {
    // ── Initialization ──

    /// @notice Initialize without PublicDelegation.
    function initialize(address _owner) external;

    /// @notice Initialize with PublicDelegation.
    function initializeWithPD(address _owner, address _publicDelegation) external;

    // ── Staking ──

    /// @notice Delegate KAIA to this contract.
    function delegate() external payable;

    // ── Unstaking ──

    /// @notice Schedule a withdrawal. Returns the request ID.
    function approveStakingWithdrawal(address _to, uint256 _value) external returns (uint256 id);

    /// @notice Cancel an approved withdrawal request.
    function cancelApprovedStakingWithdrawal(uint256 _id) external;

    /// @notice Execute withdrawal after lockup period.
    function withdrawApprovedStaking(uint256 _id) external;

    // ── Redelegation ──

    /// @notice Redelegate to another CnStaking (managed by PD).
    function redelegate(address _user, address _targetCnStaking, uint256 _value) external;

    /// @notice Handle incoming redelegation from another CnStaking.
    function handleRedelegation(address _user) external payable;

    // ── Getters ──

    function staking() external view returns (uint256);
    function unstaking() external view returns (uint256);
    function publicDelegation() external view returns (address);
    function STAKE_LOCKUP() external pure returns (uint256);
    function lastRedelegation(address _account) external view returns (uint256);
}

Note: Effective stake = staking() - unstaking() as defined in KIP-287.

CnStakingV4Factory

The factory is fully permissionless (no owner) with immutable beacon addresses. It deploys BeaconProxy instances via CREATE2, providing deterministic proxy addresses.

pragma solidity 0.8.25;

interface ICnStakingV4Factory {
    /// @notice Deploy CnStaking proxy without PublicDelegation.
    function deployCnStaking(address _owner) external returns (address cnStakingProxy);

    /// @notice Deploy CnStaking + PublicDelegation proxies atomically.
    /// @dev Requires msg.value >= INITIAL_LOCKUP (1e9 peb) to mint dead shares preventing inflation attack.
    function deployCnStakingWithPD(
        address _owner, IPublicDelegation.PDConstructorArgs memory _pdArgs
    ) external payable returns (address cnStakingProxy, address publicDelegationProxy);

    function isDeployedCnStaking(address _addr) external view returns (bool);
    function isDeployedPublicDelegation(address _addr) external view returns (bool);
    function getDeployer(address _addr) external view returns (address);

    function cnStakingBeacon() external view returns (address);
    function pdBeacon() external view returns (address);
}

PublicDelegationV2

PublicDelegationV2 enables KAIA holders to delegate their stake to validators and receive liquid staking tokens in return.

Key Changes from PublicDelegationV1

  • Transferable token: pdKAIA token must be transferable, unlike PublicDelegationV1 where transfers were prohibited.
  • Instant redelegation: Delegators must be able to redelegate their stake to a different validator instantly, without waiting for the standard 7-day withdrawal period. A per-user cooldown (lastRedelegation) prevents rapid cycling.
  • Upgradeable: The contract must be deployed as an upgradeable proxy to enable future improvements.

PublicDelegationV2 Interface

PublicDelegationV2 implements an ERC4626-like tokenized staking vault (pdKAIA). The interface extends IKIP163 from KIP-163.

pragma solidity 0.8.25;

interface IPublicDelegation is IKIP163 {
    struct PDConstructorArgs {
        address owner;
        address commissionTo;
        uint256 commissionRate;  // MAX_COMMISSION_RATE = 1e4 (100%)
        string gcName;
    }

    enum WithdrawalRequestState {
        Undefined, Requested, Withdrawable, Withdrawn, PendingCancel, Canceled
    }

    // ── Initialization ──

    function initialize(address _baseCnStaking, PDConstructorArgs memory _args) external;

    // ── Staking ──

    /// @notice Stake KAIA, receive pdKAIA shares.
    function stake() external payable;
    function stakeFor(address _recipient) external payable; // from IKIP163

    // ── Withdrawal ──

    /// @notice Withdraw exact KAIA amount. Burns ceiling shares.
    function withdraw(address _recipient, uint256 _assets) external;

    /// @notice Redeem exact shares for KAIA. Receives floor assets.
    function redeem(address _recipient, uint256 _shares) external;

    function cancelApprovedStakingWithdrawal(uint256 _requestId) external;
    function claim(uint256 _requestId) external;

    // ── Redelegation ──

    function redelegateByAssets(address _targetCnStaking, uint256 _assets) external;
    function redelegateByShares(address _targetCnStaking, uint256 _shares) external;

    // ── Owner ──

    function updateCommissionTo(address _commissionTo) external;
    function updateCommissionRate(uint256 _commissionRate) external;

    /// @notice Manually trigger reward sweep and restake.
    function sweep() external;

    // ── Getters (ERC4626-like) ──

    function baseCnStaking() external view returns (address);
    function totalAssets() external view returns (uint256);
    function convertToShares(uint256 _assets) external view returns (uint256);
    function convertToAssets(uint256 _shares) external view returns (uint256);
    function previewDeposit(uint256 _assets) external view returns (uint256);
    function previewWithdraw(uint256 _assets) external view returns (uint256);
    function previewRedeem(uint256 _shares) external view returns (uint256);
    function maxRedeem(address _owner) external view returns (uint256);
    function maxWithdraw(address _owner) external view returns (uint256);
    function commissionTo() external view returns (address);
    function commissionRate() external view returns (uint256);
}

VRank

VRank manages validator and candidate scoring based on performance metrics as defined in KIP-227. The on-chain VRank contract stores epoch scores, while the core client computes PFS/CFS and triggers state transitions via AddressBookV2.processSystemTransition().

Other contracts

  • Upgradeable: VotingV2, StakingTrackerV3, CLRegistryV2, and other contracts must be deployed as an upgradeable proxy to enable future improvements.

Rationale

Removing Embedded Multisig

The original contracts include built-in multisig functionality to provide security for validator operations. However, this approach has several drawbacks:

  • Limits flexibility in key management strategies
  • Increases contract complexity and gas costs
  • Duplicates functionality available in mature external solutions (e.g., KaiaSafe)

By removing embedded multisig, validators gain flexibility to choose their preferred security model while reducing contract complexity.

One-to-One Mapping in AddressBookV2

The previous design allows multiple staking contracts to share a reward address, with stakes consolidated for reward distribution, voting power calculation, etc:

[NodeId1, StakingContract1, RewardAddress1]
[NodeId2, StakingContract2, RewardAddress1]  // same reward address
[NodeId3, StakingContract3, RewardAddress1]  // same reward address
=> Voting power = StakingContract1 + StakingContract2 + StakingContract3

This design allows validators to manage multiple staking contracts as a single validator entity. However, the consolidation adds unnecessary complexity. In AddressBookV2, each validator entry represents a single staking contract with a unique reward address, simplifying the data model. See KIP-287 for details.

Integrating information to AddressBookV2

Storing BLS public keys and GC IDs in a separate contract was inevitable, as the existing AddressBook could not be upgraded. With AddressBookV2, they can be stored directly in a single contract, reducing the number of contract calls and providing a single source of truth for validator data.

Upgradeability

There have been several contract upgrades in the past (e.g., CnStakingV1 -> V2 -> V3, StakingTrackerV1 -> V2). Migrations often involved significant overhead; especially, CnStaking migration takes over 7 days for withdrawal. With PublicDelegation, migrations become even more complex as they require coordination with individual delegators. This degraded user experience and delayed protocol improvements.

Deploying contracts as upgradeable proxies eliminates this migration overhead, allowing protocol improvements to apply immediately. Upgrades are authorized through on-chain governance, ensuring transparent and decentralized control over contract changes.

Backwards Compatibility

Parties depending on the system contracts MUST review this document and update their implementations accordingly.

AddressBookV2

AddressBookV2 is designed to be compatible with AddressBook interface and existing consolidation logic, in order to reduce the friction with existing services. Despite this compatibility, services MUST adopt AddressBookV2 interfaces to take full advantage of the new features. Existing consolidation logic that groups stakes by reward address continues to work correctly; each group simply contains exactly one entry.

AddressBookV2 replaces the existing AddressBook at 0x0000000000000000000000000000000000000400. The address will contain proxy code starting from FORK_BLOCK.

CnStakingV4

All validators MUST migrate to CnStakingV4 before FORK_BLOCK, as AddressBookV2 only accepts staking contracts deployed by the CnStakingV4 factory. Since withdrawals require a 7-day waiting period, validators SHOULD begin their migration well in advance of FORK_BLOCK.

Each validator MUST use a single staking contract; validators with multiple staking contracts MUST consolidate before migration.

Services reading old CnStaking (V1-V3) state (such as rewardAddress or voterAddress) MUST update to read from AddressBookV2.

PublicDelegationV2

Users and contracts MUST NOT rely on the non-transferable property of delegation tokens, since pdKAIA tokens become transferable in PublicDelegationV2.

On-chain information

Users MUST read on-chain information from AddressBookV2 after FORK_BLOCK.

References

Copyright and related rights waived via CC0.