Important
The ckSOL minter is under active development and subject to change. Access to the repository has been opened to allow for early feedback. Check back regularly for updates.
ckSOL is a chain-key token on the Internet Computer that is backed 1:1 by SOL, the native token of the Solana blockchain.
Each ckSOL is backed by exactly 1 SOL held by the ckSOL minter canister. ckSOL can be converted to SOL at any time, and vice versa.
- How It Works
- Architecture
- Deployment
- Interacting via the CLI
- Repository Structure
- Development
- Related Projects
- Contributing
- License
The ckSOL minter canister is the core component of the system. It manages the conversion between SOL and ckSOL, maintains custody of the SOL backing all outstanding ckSOL tokens, and interacts with the Solana blockchain via the SOL RPC canister.
The ckSOL token itself is implemented as an ICRC-1/ICRC-2 ledger canister.
The minter controls one or more Solana addresses derived from a threshold Schnorr over Ed25519 public key and a per-account derivation path. No private key ever exists in plaintext β Solana transactions are signed via the IC management canister's sign_with_schnorr API (SchnorrAlgorithm::Ed25519).
-
Get a deposit address. Call
get_deposit_addresson the minter with your ICP principal (and an optional subaccount). The minter returns a Solana address derived specifically for your account. -
Send SOL. Transfer SOL to that deposit address from any Solana wallet.
-
Notify the minter. Call
process_depositon the minter with the Solana transaction signature and the same owner/subaccount used in step 1. This call requires attaching cycles (seeprocess_deposit_required_cyclesinget_minter_info). The minter:- Fetches the transaction from Solana via the SOL RPC canister.
- Verifies it is a valid transfer to your deposit address.
- Mints the corresponding amount of ckSOL (minus the deposit fee) to your ICRC-1 ledger account.
-
Consolidation. The minter periodically consolidates funds from individual deposit addresses into its main Solana account.
sequenceDiagram
actor User
participant Minter as ckSOL Minter
participant Ledger as ckSOL Ledger
participant Solana
User->>Minter: get_deposit_address(owner, subaccount)
Minter-->>User: deposit_address
User->>Solana: transfer SOL to deposit_address
Solana-->>User: tx_signature
User->>Minter: process_deposit(owner, subaccount, signature)
Minter->>Solana: fetch & verify transaction
Minter->>Ledger: mint with icrc1_transfer(to=user, amount - deposit_fee)
Ledger-->>Minter: block_index
Minter-->>User: Minted { block_index, minted_amount }
-
Approve the minter. Grant the minter an ICRC-2 allowance on your ckSOL ledger account.
-
Submit a withdrawal request. Call
withdrawon the minter with the destination Solana address and the amount in lamports. The minter:- Burns the requested ckSOL from your ledger account via icrc2_transfer_from.
- Queues the corresponding SOL transfer.
-
Transaction submission. The minter constructs a Solana transaction, signs it using chain-key Ed25519, and submits it via the SOL RPC canister.
-
Monitor status. Call
withdrawal_statuswith the ledger burn index returned bywithdrawto track the status of your withdrawal request (PendingβTxSentβTxFinalized).
sequenceDiagram
actor User
participant Minter as ckSOL Minter
participant Ledger as ckSOL Ledger
participant Solana
User->>Ledger: icrc2_approve(spender=minter, amount)
Ledger-->>User: ok
User->>Minter: withdraw(destination_address, amount)
Minter->>Ledger: burn with icrc2_transfer_from(from=user, to=burn, amount)
Ledger-->>Minter: burn_block_index
Minter-->>User: burn_block_index
Note over Minter,Solana: (processed asynchronously by the minter)
Minter->>Solana: submit SOL transfer to destination_address
User->>Minter: withdrawal_status(burn_block_index)
Minter-->>User: TxFinalized(Success)
graph TD
subgraph IC["Internet Computer"]
Minter["ckSOL Minter\n(this repo)"]
Ledger["ckSOL Ledger"]
RPC["SOL RPC Canister"]
Minter --> Ledger
Minter --> RPC
end
RPC -->|HTTPS outcalls| Providers["Solana JSON-RPC providers\n(Ankr, Helius, dRPC, ...)"]
Providers --> Solana["Solana Blockchain"]
ckSOL Minter β The main canister in this repository. It manages the deposit and withdrawal lifecycle, holds custody of SOL via chain-key addresses, signs Solana transactions using threshold Schnorr over Ed25519, and interacts with the ckSOL ledger.
ckSOL Ledger β A standard ICRC-1/ICRC-2 ledger canister. It tracks ckSOL balances and processes mints and burns as instructed by the minter.
SOL RPC Canister β A shared infrastructure canister on the Internet Computer that relays Solana JSON-RPC calls to multiple providers via HTTPS outcalls and aggregates their responses. See the SOL RPC canister repository for details.
| Canister | Canister ID |
|---|---|
| Minter | ljyxk-riaaa-aaaar-qb5mq-cai |
| Ledger | la34w-haaaa-aaaar-qb5na-cai |
| Canister | Canister ID |
|---|---|
| Minter | lh22c-kyaaa-aaaar-qb5nq-cai (not yet deployed) |
| Ledger | ls5lp-lqaaa-aaaar-qb5oa-cai (not yet deployed) |
You can interact with the ckSOL minter using icp-cli. Pass -e prod (or -e staging) to target the corresponding environment defined in icp.yaml.
Query fees, minimum amounts, and the current minter balance:
icp canister call -e prod cksol_minter get_minter_info '()'Returns the Solana address you should send SOL to in order to deposit. When owner is null, it defaults to your calling identity's principal:
icp canister call -e prod cksol_minter get_deposit_address \
'(record { owner = null; subaccount = null })' --queryAfter sending SOL to your deposit address, call process_deposit with the Solana transaction signature to trigger minting. Pass the same owner/subaccount used when calling get_deposit_address β when owner is null, it defaults to your calling identity's principal. Replace <SIGNATURE> with the base-58 encoded transaction signature.
Note
This call requires attaching cycles β check the required amount via get_minter_info (process_deposit_required_cycles field). If your identity does not hold cycles directly, you can convert ICP to cycles first, or route the call through a proxy canister using --proxy <proxy-principal> --cycles <amount>.
icp canister call -e prod cksol_minter process_deposit \
'(record { owner = null; subaccount = null; signature = "<SIGNATURE>" })'A successful response looks like:
(variant { Ok = variant { Minted = record { block_index = 42; minted_amount = 990_000_000; deposit_id = ... } } })
Burns ckSOL from your ledger account and initiates a transfer of the equivalent SOL to the given Solana address. Replace <SOLANA_ADDRESS> with the destination address and <AMOUNT> with the amount in lamports. The optional from_subaccount field defaults to null (the default subaccount):
icp canister call -e prod cksol_minter withdraw \
'(record { address = "<SOLANA_ADDRESS>"; amount = <AMOUNT>; from_subaccount = null })'A successful response returns the burn block index, which you can use to track the withdrawal:
(variant { Ok = record { block_index = 42 } })
After calling withdraw, track the status using the block_index returned in the response:
icp canister call -e prod cksol_minter withdrawal_status \
'(record { block_index = 42 })'.
βββ minter/ # ckSOL minter canister
β βββ src/
β β βββ address/ # Deposit address derivation
β β βββ consolidate/ # Deposit consolidation logic
β β βββ dashboard/ # HTTP dashboard
β β βββ lifecycle.rs # Canister init/upgrade and event log
β β βββ metrics.rs # Prometheus metrics
β β βββ monitor/ # Transaction monitoring
β β βββ state/ # Minter state and event sourcing
β β βββ deposit/manual/ # Manual deposit processing
β β βββ withdraw/ # Withdrawal processing
β β βββ ...
β βββ cksol_minter.did # Candid interface
βββ libs/
β βββ types/ # Public ckSOL types (cksol-types crate)
β βββ types-internal/ # Internal types and event definitions
βββ integration_tests/ # End-to-end tests using PocketIC
βββ scripts/
βββ build # Build script for the minter Wasm
βββ bootstrap # Install build dependencies
- Rust β the correct toolchain version is pinned in
rust-toolchain.toml. ic-wasmversion 0.3.5 β used for Wasm post-processing.jqβ used by./scripts/buildto generate Wasm metadata.gzipβ used by./scripts/buildto compress the output Wasm.
Install the Rust toolchain and ic-wasm by running:
./scripts/bootstrapBuild the minter Wasm:
./scripts/build --cksol_minterThe resulting cksol_minter.wasm.gz will be written to the repository root.
You can also build the Rust workspace directly (without Wasm post-processing):
cargo buildThe test suite has two parts:
Unit tests and PocketIC integration tests β no external dependencies:
cargo test --lib
cargo test -p cksol-int-tests --test testsSolana validator integration tests β require a running solana-test-validator at http://localhost:8899:
solana-test-validator &
cargo test -p cksol-int-tests --test solana_test_validatorCaution
Running cargo test without arguments will attempt all tests, including the Solana validator suite, and will fail if no validator is running.
- SOL RPC Canister β Solana JSON-RPC access from the Internet Computer.
- ckETH β Chain-key Ethereum token, which ckSOL is modeled after.
- ckBTC β Chain-key Bitcoin token.
- ICRC-1 Ledger β The token standard used by the ckSOL ledger.
At this point we do not accept external contributions yet. External contributions will be accepted after the initial release.
This project is licensed under the Apache License 2.0.