KIP 290: Permissionless Smart Contracts
| Author | Lewis, Ian, Ollie, Lake, and Joseph |
|---|---|
| Discussions-To | https://github.com/kaiachain/kips/issues/90 |
| Status | Draft |
| Type | Core |
| Created | 2026-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:
nodeIdis used as the mapping key in AddressBookV2 and is not stored inside theNodeInfostruct. ThetimeoutAtfield records the expiry timestamp forValPaused(pause timeout) andValInactive/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
- KIP-81: Implementing the on-chain governance voting method
- KIP-113: BLS public key registry
- KIP-226: Consensus liquidity for Kaia
- KIP-227: Candidate and Validator Evaluation
- KIP-277: Self Validator Registration
- KIP-286: Permissionless Validator Lifecycle
- KIP-287: Permissionless Staking Policy
Copyright
Copyright and related rights waived via CC0.