Deployment
Minter’s address on Solana
Given a canister ID, the address controlled by the minter can be derived offline without installing any code.
See should_derive_mainnet_minter_addresses_offline for an example.
| Environment | Canister ID | Solana address |
|---|---|---|
| Production | lh22c-kyaaa-aaaar-qb5nq-cai | GXVewvv6HehcLFYCmwpqh5CrMjqN9zJ8rqzGq3HEt9Ax |
| Staging | ljyxk-riaaa-aaaar-qb5mq-cai | Br8eRkeya8hy3sHCYqtWqGeNNZ349aPSUKGVYFWKKer1 |
Create nonce accounts
Withdrawals are sent as durable-nonce transactions, so the minter needs a pool of durable nonce accounts whose nonce authority is the minter’s address.
Without them, withdraw_sol returns TemporarilyUnavailable("The durable nonce account pool is empty, no withdrawal can be processed").
The commands below use the Solana CLI and the staging minter on Devnet.
For production, use the production minter’s address and --url mainnet-beta.
Fund a fee payer that pays for the rent of the nonce accounts:
solana-keygen new --no-bip39-passphrase -o ~/.config/solana/devnet-payer.json
solana airdrop 1 --keypair ~/.config/solana/devnet-payer.json --url devnet
Create the nonce accounts with the minter as nonce authority:
MINTER_ADDRESS=Br8eRkeya8hy3sHCYqtWqGeNNZ349aPSUKGVYFWKKer1
mkdir -p nonce-accounts
for i in 1 2 3; do
solana-keygen new --no-bip39-passphrase --silent -o nonce-accounts/nonce-$i.json
solana create-nonce-account nonce-accounts/nonce-$i.json 0.002 \
--nonce-authority $MINTER_ADDRESS \
--keypair ~/.config/solana/devnet-payer.json \
--url devnet
done
Each account is funded with 0.002 SOL, above the rent-exempt minimum for the 80 bytes of a nonce account (see solana rent 80).
The nonce-$i.json keypairs are only needed to create the accounts: afterwards, only the nonce authority can advance the nonce or withdraw from the account.
Each in-flight withdrawal transaction occupies one nonce account until it is finalized, and a transaction batches up to 10 withdrawals.
Check that the nonce authority of each account is the minter’s address:
for i in 1 2 3; do
ADDRESS=$(solana-keygen pubkey nonce-accounts/nonce-$i.json)
echo $ADDRESS
solana nonce-account $ADDRESS --url devnet
done
The minter only parses the addresses when they are added and verifies each account when it creates a withdrawal transaction, so a wrong nonce authority only surfaces once a withdrawal is processed.
Add the accounts to the minter, either in nonce_accounts of the initialization arguments or with an upgrade:
(
variant {
Upgrade = record {
nonce_accounts_to_add = opt vec { "<address-1>"; "<address-2>"; "<address-3>" };
}
},
)
The other fields of UpgradeArgs are optional and can be omitted.
Minter
Initialization arguments:
| Production | Staging |
|---|---|
|
|
The fees and minimum amounts follow Section 3.3 of the design:
| Argument | Value | Rationale |
|---|---|---|
deposit_sol_fee | 45B cycles | Covers the threshold signature and the RPC calls of a sweep containing a single deposit. |
deposit_sol_required_cycles | 1T cycles | Must be at least GET_BALANCE_CYCLES (10B) plus deposit_sol_fee; unused cycles are refunded. |
minimum_deposit_amount | 0.02 SOL (20,000,000) | Must be at least twice the rent exemption threshold plus the fee of one signature. |
withdrawal_fee | 0.001 SOL (1,000,000) | Covers getAccountInfo, sendTransaction, getTransaction and the threshold signature. |
minimum_withdrawal_amount | 0.002 SOL (2,000,000) | Must be at least the withdrawal fee plus the rent exemption threshold. |
nonce_accounts | empty | Replace with the addresses created in Create nonce accounts. |
Ledger
Initialization arguments:
| Production | Staging |
|---|---|
|
|
LOGO is the base64 encoding of the token logo, without line breaks.
For production, use LOGO=$(base64 -i static/images/cksol-token.svg | tr -d '\n').
For staging, use LOGO=$(base64 -i static/images/ckdevnetsol-token.svg | tr -d '\n').
The minting account is the minter’s default account and the fee collector is the minter’s subaccount 0x0fee.
The transfer fee of 500 lamports follows Section 3.3.1 of the design.
Archiving is effectively disabled by setting trigger_threshold to 4,200,000,000 blocks.
The other archive options match those of the ckETH ledger (proposal 126170), so that archives are controlled by the NNS root canister if archiving is enabled by a later upgrade.
Index
Initialization arguments:
| Production | Staging |
|---|---|
|
|
The index takes an optional argument, hence the leading opt.
Leaving retrieve_blocks_from_ledger_interval_seconds unset uses the index’s default polling interval.
Test
The commands below use the staging minter and ledger. For production, replace the canister IDs with those of the production minter and ledger.
MINTER=ljyxk-riaaa-aaaar-qb5mq-cai
LEDGER=la34w-haaaa-aaaar-qb5na-cai
OWNER=$(icp identity principal --identity demo)
Deposit SOL
Get the deposit address
icp canister call $MINTER get_deposit_address "(record { owner = opt principal \"$OWNER\"; subaccount = null })" -n ic
Send SOL to the returned address (faucet).
The balance of the deposit address must be at least minimum_deposit_amount, which get_minter_info returns:
icp canister call $MINTER get_minter_info '()' --query -n ic
Queue the deposit
deposit_sol requires deposit_sol_required_cycles (1T cycles) to be attached to the call.
Since an identity cannot attach cycles, route the call through a proxy canister that holds cycles:
icp canister call $MINTER deposit_sol "(record { owner = opt principal \"$OWNER\"; subaccount = null })" \
--proxy h35ft-riaaa-aaaar-qb37a-cai \
--cycles 1000000000000 \
--identity hsm \
--identity-password-file ~/.config/icp/hsm.pin \
-n ic
The minter sees the proxy canister as the caller, so owner must be set explicitly.
Otherwise, the ckSOL would be minted to the proxy canister.
The call returns the deposit ID, e.g. (variant { Ok = 0 : nat64 }).
Check the deposit status
icp canister call $MINTER deposit_sol_status '(0 : nat64)' --query -n ic
The status goes through Queued, Swept, Finalized and Minted.
Once minted, the ckSOL balance of the owner is:
icp canister call $LEDGER icrc1_balance_of "(record { owner = principal \"$OWNER\"; subaccount = null })" --query -n ic
Withdraw SOL
Withdrawals require nonce accounts, see Create nonce accounts.
Without them, withdraw_sol returns TemporarilyUnavailable and burns nothing.
Approve the minter
The minter burns the withdrawn ckSOL with icrc2_transfer_from, so the owner must first approve the minter for the amount to withdraw.
The approval costs the ledger transfer fee of 500 lamports, so withdrawing the whole balance means withdrawing the balance minus 500 lamports:
AMOUNT=2000000
DESTINATION=6TqNg48mSd5evmY66JVfGeGTwszrU1YLCeSw3GJ2qsUC
icp canister call $LEDGER icrc2_approve "(record { spender = record { owner = principal \"$MINTER\"; subaccount = null }; amount = $AMOUNT : nat; from_subaccount = null; expected_allowance = null; expires_at = null; fee = null; memo = null; created_at_time = null })" --identity demo -n ic
Withdraw
icp canister call $MINTER withdraw_sol "(record { from_subaccount = null; amount = $AMOUNT : nat64; address = \"$DESTINATION\" })" --identity demo -n ic
The amount must be at least minimum_withdrawal_amount, and the destination receives the amount minus withdrawal_fee.
The call returns the index of the burn transaction on the ledger, e.g. (variant { Ok = record { block_index = 2 : nat64 } }).
Check the withdrawal status
icp canister call $MINTER withdraw_sol_status '(record { block_index = 2 : nat64 })' -n ic
The status goes through Pending, TxSent and TxFinalized.