Skip to main content

UnStake

caution

We didn't switch protocol to second version on mainnet yet. So, this code will work only after migration. Please see first version if you are going to deploy integration before breaking upgrade. Anyway, you should be ready for migration, so we recommend follow our example.

info

We highly recommend use our SDK, so we could support you better in case of some problems. Also, integration with SDK is much easier & more simple than manually.

Step 1: Fetching Lido program state to retrieve relevant data

const accountInfo = await connection.getAccountInfo(LIDO_ADDRESS);

Use getAccountInfo(LIDO_ADDRESS) function from this library to fetch the Lido program data (in the form of a buffer).

  • Create a borsh schema to deserialize the solido state data:
class Lido {
constructor(data) {
Object.assign(this, data);
}
}

class RewardDistribution {
constructor(data) {
Object.assign(this, data);
}
}

class FeeRecipients {
constructor(data) {
Object.assign(this, data);
}
}

class ExchangeRate {
constructor(data) {
Object.assign(this, data);
}
}

class Metrics {
constructor(data) {
Object.assign(this, data);
}
}

class LamportsHistogram {
constructor(data) {
Object.assign(this, data);
}
}

class WithdrawMetric {
constructor(data) {
Object.assign(this, data);
}
}

const accountInfoScheme = new Map([
[
ExchangeRate,
{
kind: 'struct',
fields: [
['computed_in_epoch', 'u64'],
['st_sol_supply', 'u64'],
['sol_balance', 'u64'],
],
},
],
[
LamportsHistogram,
{
kind: 'struct',
fields: [
['counts1', 'u64'],
['counts2', 'u64'],
['counts3', 'u64'],
['counts4', 'u64'],
['counts5', 'u64'],
['counts6', 'u64'],
['counts7', 'u64'],
['counts8', 'u64'],
['counts9', 'u64'],
['counts10', 'u64'],
['counts11', 'u64'],
['counts12', 'u64'],
['total', 'u64'],
],
},
],
[
WithdrawMetric,
{
kind: 'struct',
fields: [
['total_st_sol_amount', 'u64'],
['total_sol_amount', 'u64'],
['count', 'u64'],
],
},
],
[
Metrics,
{
kind: 'struct',
fields: [
['fee_treasury_sol_total', 'u64'],
['fee_validation_sol_total', 'u64'],
['fee_developer_sol_total', 'u64'],
['st_sol_appreciation_sol_total', 'u64'],
['fee_treasury_st_sol_total', 'u64'],
['fee_validation_st_sol_total', 'u64'],
['fee_developer_st_sol_total', 'u64'],
['deposit_amount', LamportsHistogram],
['withdraw_amount', WithdrawMetric],
],
},
],
[
RewardDistribution,
{
kind: 'struct',
fields: [
['treasury_fee', 'u32'],
['developer_fee', 'u32'],
['st_sol_appreciation', 'u32'],
],
},
],
[
FeeRecipients,
{
kind: 'struct',
fields: [
['treasury_account', [32]],
['developer_account', [32]],
],
},
],
[
Lido,
{
kind: 'struct',
fields: [
['account_type', 'u8'],

['lido_version', 'u8'],

['manager', [32]],

['st_sol_mint', [32]],

['exchange_rate', ExchangeRate],

['sol_reserve_authority_bump_seed', 'u8'],
['stake_authority_bump_seed', 'u8'],
['mint_authority_bump_seed', 'u8'],

['reward_distribution', RewardDistribution],

['fee_recipients', FeeRecipients],

['metrics', Metrics],

['validator_list', [32]],

['maintainer_list', [32]],

['max_commission_percentage', 'u8'],
],
},
],
]);
  • Deserialize the data using above schema:
import { deserializeUnchecked } from 'borsh';
// It deserializes object from bytes using schema, without checking the length read
const deserializedAccountInfo = deserializeUnchecked(
accountInfoschema,
Lido,
accountInfo.data,
);

Step 2: Fetching Validators list account

const validatorsList = new PublicKey(deserializedAccountInfo.validators_list);
const validators = await connection.getAccountInfo(validatorsList);
  • Create a borsh schema to deserialize the validators data:
class ListHeader {
constructor(data) {
Object.assign(this, data);
}
}

class SeedRange {
constructor(data) {
Object.assign(this, data);
}
}

class ValidatorClass {
constructor(data) {
Object.assign(this, data);
}
}

class AccountList {
constructor(data) {
Object.assign(this, data);
}
}

const validatorsSchema = new Map([
[
ListHeader,
{
kind: 'struct',
fields: [
['account_type', 'u8'],
['lido_version', 'u8'],
['max_entries', 'u32'],
],
},
],
[
SeedRange,
{
kind: 'struct',
fields: [
['begin', 'u64'],
['end', 'u64'],
],
},
],
[
ValidatorClass,
{
kind: 'struct',
fields: [
['vote_account_address', [32]],
['stake_seeds', SeedRange],
['unstake_seeds', SeedRange],
['stake_accounts_balance', 'u64'],
['unstake_accounts_balance', 'u64'],
['effective_stake_balance', 'u64'],
['active', 'u8'],
],
},
],
[
AccountList,
{
kind: 'struct',
fields: [
['header', ListHeader],
['entries', [ValidatorClass]],
],
},
],
]);
  • Deserialize the data using above schema:
const deserializedValidators = deserializeUnchecked(
validatorsSchema,
AccountList,
validators.data,
);

Step 3: Sign new Transaction

const newStakeAccount = Keypair.generate();
// Create new transaction
const transaction = new Transaction({ feePayer: payer });
// Set recent blockhash
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;

Step 4: Create Withdraw Instruction

  • Create the buffer layout in the format of { instruction_code: 1 byte, amount: 8 bytes, validator_index: 4 bytes}:
import { nu64, struct, u32, u8 } from 'buffer-layout';

const dataLayout = struct([u8('instruction'), nu64('amount'), u32('validator_index')]);

const data = Buffer.alloc(dataLayout.span);
  • Get heaviest validator index:
const getHeaviestValidator = (validatorEntries) => {
const sortedValidators = validatorEntries.sort(
(validatorA, validatorB) =>
validatorB.stake_accounts_balance.toNumber() - validatorA.stake_accounts_balance.toNumber(),
);

return sortedValidators[0];
};

const getValidatorIndex = (validatorEntries, validator) =>
validatorEntries.findIndex(
({ vote_account_address: voteAccountAddress }) =>
voteAccountAddress.toString() === validator.vote_account_address.toString(),
);

const validator = getHeaviestValidator(validators);

const validatorIndex = getValidatorIndex(validators, validator);
  • Encode the deposit data using the buffer layout:
import BN from 'bn.js';

dataLayout.encode(
{
instruction: 23, // code of withdraw instruction
amount: new BN(amount),
validator_index: validatorIndex,
},
data,
);
  • Set all keys for the deposit instruction using the program data we fetch earlier:
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
Keypair,
PublicKey,
StakeProgram,
SystemProgram,
SYSVAR_CLOCK_PUBKEY,
} from '@solana/web3.js';

const calculateStakeAuthority = async (lidoAddress, programId) => {
const bufferArray = [lidoAddress.toBuffer(), Buffer.from('stake_authority')];

const mint = await PublicKey.findProgramAddress(bufferArray, programId);

return mint[0];
};

const calculateStakeAccountAddress = async (lidoAddress, programId, validatorVoteAccount, seed) => {
const bufferArray = [
lidoAddress.toBuffer(),
validatorVoteAccount.toBuffer(),
Buffer.from('validator_stake_account'),
seed.toArray('le', 8),
];

const [stakeAccountAddress] = await PublicKey.findProgramAddress(bufferArray, programId);

return stakeAccountAddress;
};

const getStSolAccountsForUser = async (owner) => {
const stSolAccounts = [];

const { value } = await connection.getParsedTokenAccountsByOwner(owner, {
mint: ST_SOL_MINT,
});

value.forEach((v) => {
const address = v.pubkey;
const balanceInLamports = parseInt(v.account.data.parsed?.info?.tokenAmount?.amount ?? '0', 10);

stSolAccounts.push({ address, balanceInLamports });
});

return stSolAccounts;
};

const stakeAuthority = await calculateStakeAuthority(lidoAddress, programId);
const validator = await getHeaviestValidator(validators);

const senderStSolAccountAddress = await getStSolAccountsForUser(wallet.publicKey); // payerAddress

const validatorStakeAccount = await calculateStakeAccountAddress(
LIDO_ADDRESS,
PROGRAM_ID,
new PublicKey(validator.vote_account_address),
validator.stake_seeds.begin,
);

const keys = [
{ pubkey: solidoInstanceId, isSigner: false, isWritable: true },
{ pubkey: payerAddress, isSigner: true, isWritable: false },
{ pubkey: senderStSolAccountAddress, isSigner: false, isWritable: true },
{ pubkey: ST_SOL_MINT, isSigner: false, isWritable: true },
{ pubkey: new PublicKey(validator.vote_account_address), isSigner: false, isWritable: false },
{ pubkey: validatorStakeAccount, isSigner: false, isWritable: true },
{ pubkey: stakeAccount, isSigner: true, isWritable: true },
{ pubkey: stakeAuthority, isSigner: false, isWritable: false },
{ pubkey: validatorsList, isSigner: false, isWritable: true }, // step 2
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
];
  • Add the instruction to the transaction:
transaction.add(
new TransactionInstruction({
keys,
programId: PROGRAM_ID,
data,
}),
);

Step 5: Create deactivate transaction & add its instructions to transaction

import { StakeProgram } from '@solana/web3.js';

const deactivateTransaction = StakeProgram.deactivate({
authorizedPubkey: payer,
stakePubkey: newStakeAccount.publicKey,
});

transaction.add(...deactivateTransaction.instructions);

transaction.partialSign(newStakeAccount); // step 2 variables