@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,341 lines (1,215 loc) • 246 kB
text/typescript
import BN from 'bn.js';
import {
Account,
AccountRole,
Address,
address,
Base58EncodedBytes,
fetchEncodedAccount,
generateKeyPairSigner,
getAddressEncoder,
getBase58Decoder,
GetProgramAccountsDatasizeFilter,
GetProgramAccountsMemcmpFilter,
getProgramDerivedAddress,
AccountMeta,
Instruction,
lamports,
ProgramDerivedAddress,
Rpc,
Slot,
SolanaRpcApi,
TransactionSigner,
AccountInfoWithPubkey,
AccountInfoBase,
AccountInfoWithJsonData,
Option,
some,
none,
} from '@solana/kit';
import {
AllOracleAccounts,
CdnResources,
CdnResourcesResponse,
DEFAULT_PUBLIC_KEY,
DEFAULT_RECENT_SLOT_DURATION_MS,
getAssociatedTokenAddress,
getTokenBalanceFromAccountInfoLamports,
getTokenOracleData,
getTransferWsolIxs,
KaminoMarket,
KaminoReserve,
KVaultGlobalConfig,
lamportsToDecimal,
Reserve,
WRAPPED_SOL_MINT,
} from '../lib';
import {
addUpdateWhitelistedReserve,
AddUpdateWhitelistedReserveAccounts,
AddUpdateWhitelistedReserveArgs,
buy,
BuyAccounts,
BuyArgs,
deposit,
DepositAccounts,
DepositArgs,
giveUpPendingFees,
GiveUpPendingFeesAccounts,
GiveUpPendingFeesArgs,
initKVaultGlobalConfig,
initVault,
InitVaultAccounts,
invest,
InvestAccounts,
removeAllocation,
RemoveAllocationAccounts,
sell,
SellAccounts,
SellArgs,
updateAdmin,
UpdateAdminAccounts,
updateKVaultGlobalConfig,
UpdateKVaultGlobalConfigAccounts,
UpdateKVaultGlobalConfigArgs,
updateReserveAllocation,
UpdateReserveAllocationAccounts,
UpdateReserveAllocationArgs,
updateVaultConfig,
UpdateVaultConfigAccounts,
UpdateVaultConfigArgs,
withdraw,
WithdrawAccounts,
WithdrawArgs,
withdrawFromAvailable,
WithdrawFromAvailableAccounts,
WithdrawFromAvailableArgs,
withdrawPendingFees,
WithdrawPendingFeesAccounts,
} from '../@codegen/kvault/instructions';
import {
UpdateGlobalConfigMode,
UpdateKVaultGlobalConfigModeKind,
UpdateReserveWhitelistModeKind,
VaultConfigField,
VaultConfigFieldKind,
} from '../@codegen/kvault/types';
import { ReserveWhitelistEntry, VaultState } from '../@codegen/kvault/accounts';
import Decimal from 'decimal.js';
import { bpsToPct, decodeVaultName, numberToLamportsDecimal, parseTokenSymbol, pubkeyHashMapToJson } from './utils';
import { PROGRAM_ID } from '../@codegen/klend/programId';
import { ReserveWithAddress } from './reserve';
import { Fraction } from './fraction';
import {
CDN_ENDPOINT,
createAtasIdempotent,
createWsolAtaIfMissing,
getAllStandardTokenProgramTokenAccounts,
getKVaultSharesMetadataPda,
getTokenAccountAmount,
getTokenAccountMint,
lendingMarketAuthPda,
parseBooleanFlag,
programDataPda,
SECONDS_PER_YEAR,
U64_MAX,
VAULT_INITIAL_DEPOSIT,
} from '../utils';
import { getAccountOwner, getProgramAccounts } from '../utils';
import {
AcceptVaultOwnershipIxs,
AllDepositAccounts,
AllWithdrawAccounts,
APYs,
CreateVaultFarm,
DepositIxs,
DisinvestAllReservesIxs,
InitVaultIxs,
ReserveAllocationOverview,
SyncVaultLUTIxs,
UpdateReserveAllocationIxs,
UpdateVaultConfigIxs,
UserSharesForVault,
VaultComputedAllocation,
VaultReleaseCheckResult,
WithdrawAndBlockReserveIxs,
WithdrawIxs,
} from './vault_types';
import { batchFetch, collToLamportsDecimal, ZERO } from '@kamino-finance/kliquidity-sdk';
import { FullBPSDecimal } from '@kamino-finance/kliquidity-sdk/dist/utils/CreationParameters';
import {
FarmConfigOption,
FarmIncentives,
FarmState,
getFarmIncentivesWithExistentState,
getUserStatePDA,
scaleDownWads,
} from '@kamino-finance/farms-sdk/dist';
import { getAccountsInLut, initLookupTableIx, insertIntoLookupTableIxs } from '../utils';
import {
FARMS_ADMIN_MAINNET,
FARMS_GLOBAL_CONFIG_DEVNET,
FARMS_GLOBAL_CONFIG_MAINNET,
getFarmStakeIxs,
getFarmUnstakeAndWithdrawIxs,
getSharesInFarmUserPosition,
getUserPendingRewardsInFarm,
getUserSharesInTokensStakedInFarm,
} from './farm_utils';
import { getCreateAccountInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { getInitializeKVaultSharesMetadataIx, getUpdateSharesMetadataIx, resolveMetadata } from '../utils/metadata';
import { decodeReserveWhitelistEntry, decodeVaultState } from '../utils/vault';
import { fetchMaybeToken, findAssociatedTokenPda, getCloseAccountInstruction } from '@solana-program/token-2022';
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import { SYSVAR_INSTRUCTIONS_ADDRESS, SYSVAR_RENT_ADDRESS } from '@solana/sysvars';
import { noopSigner } from '../utils/signer';
import { Farms, UserState } from '@kamino-finance/farms-sdk';
import { computeReservesAllocation } from '../utils/vaultAllocation';
import { getReserveFarmRewardsAPY } from '../utils/farmUtils';
import { fetchKaminoCdnData } from '../utils/readCdnData';
import { walletIsSquadsMultisig } from '../utils/multisig';
import { RiskManagerInfo } from '../models/cdn';
import {
updateGlobalConfigAdmin,
UpdateGlobalConfigAdminAccounts,
} from '../@codegen/kvault/instructions/updateGlobalConfigAdmin';
export const kaminoVaultId = address('KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd');
export const kaminoVaultStagingId = address('stKvQfwRsQiKnLtMNVLHKS3exFJmZFsgfzBPWHECUYK');
const TOKEN_VAULT_SEED = 'token_vault';
const CTOKEN_VAULT_SEED = 'ctoken_vault';
const BASE_VAULT_AUTHORITY_SEED = 'authority';
const SHARES_SEED = 'shares';
const EVENT_AUTHORITY_SEED = '__event_authority';
export const METADATA_SEED = 'metadata';
const GLOBAL_CONFIG_STATE_SEED = 'global_config';
const WHITELISTED_RESERVES_SEED = 'whitelisted_reserves';
export const METADATA_PROGRAM_ID: Address = address('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
export const INITIAL_DEPOSIT_LAMPORTS = 1000;
export const DEFAULT_CU_PER_TX = 1_400_000;
const addressEncoder = getAddressEncoder();
const base58Decoder = getBase58Decoder();
/**
* KaminoVaultClient is a class that provides a high-level interface to interact with the Kamino Vault program.
*/
export class KaminoVaultClient {
private readonly _rpc: Rpc<SolanaRpcApi>;
private readonly _kaminoVaultProgramId: Address;
private readonly _kaminoLendProgramId: Address;
private readonly _farmsProgramId?: Address;
recentSlotDurationMs: number;
// CDN cache
private _cdnResources?: CdnResources;
private _cdnResourcesPromise?: Promise<CdnResources | undefined>;
constructor(
rpc: Rpc<SolanaRpcApi>,
recentSlotDurationMs: number,
kaminoVaultprogramId?: Address,
kaminoLendProgramId?: Address,
cdnResources?: CdnResources,
farmsProgramId?: Address
) {
this._rpc = rpc;
this.recentSlotDurationMs = recentSlotDurationMs;
this._kaminoVaultProgramId = kaminoVaultprogramId ? kaminoVaultprogramId : kaminoVaultId;
this._kaminoLendProgramId = kaminoLendProgramId ? kaminoLendProgramId : PROGRAM_ID;
this._farmsProgramId = farmsProgramId;
this._cdnResources = cdnResources;
}
getConnection() {
return this._rpc;
}
getProgramID() {
return this._kaminoVaultProgramId;
}
getRpc() {
return this._rpc;
}
hasFarm() {
return;
}
private async loadCdnResourcesOnce(): Promise<CdnResources | undefined> {
if (this._cdnResources) {
return this._cdnResources;
}
if (this._cdnResourcesPromise) {
return this._cdnResourcesPromise;
}
this._cdnResourcesPromise = (async () => {
const response = await fetch(`${CDN_ENDPOINT}/resources.json`);
if (!response.ok) {
console.error(`Failed to fetch CDN resources: ${response.status} ${response.statusText}`);
return undefined;
}
const raw = (await response.json()) as CdnResourcesResponse;
const delegatedVaultFarms = raw['mainnet-beta']?.delegatedVaultFarms;
if (!delegatedVaultFarms) {
return undefined;
}
const riskManagers = raw['mainnet-beta']?.riskManagers ?? {};
const parsed: CdnResources = { delegatedVaultFarms, riskManagers };
this._cdnResources = parsed;
return parsed;
})();
return this._cdnResourcesPromise;
}
/**
* Check if a vault has all the needed criteria to be released
* - owner is multisig
* - vaultFarm is set and it is a farm that is valid
* - FLC farm is set and it is a farm that is valid (warning if not)
* - check shares token metadata is set
* - Check min deposit is not 0
* - Check the vault has at least one allocation
* - Check there are allocations with weight > 0 and cap > 0 (and give warning for each allocation which doesn't have cap == u64::MAX)
* - Check CDN (using loadCdnResourcesOnce) that the vaultAdmin exists in the list of admins and has a description
* @param vault - the vault to check
* @returns - a promise that resolves to the release status of the vault
*/
async checkVaultReleaseStatus(vault: KaminoVault): Promise<VaultReleaseCheckResult> {
const result: VaultReleaseCheckResult = {
errors: [],
warnings: [],
success: true,
};
const vaultState = await vault.getState();
// 1. Check owner is multisig
try {
const isMultisig = await walletIsSquadsMultisig(vaultState.vaultAdminAuthority);
if (!isMultisig) {
result.errors.push(`Vault admin ${vaultState.vaultAdminAuthority} is not a Squads multisig`);
}
} catch (e) {
result.errors.push(`Failed to check if vault admin ${vaultState.vaultAdminAuthority} is a multisig: ${e}`);
}
// 2. Check vaultFarm is set and valid
if (vaultState.vaultFarm === DEFAULT_PUBLIC_KEY) {
result.errors.push('Vault farm is not set');
} else {
const farmState = await FarmState.fetch(this._rpc, vaultState.vaultFarm);
if (!farmState) {
result.errors.push(`Vault farm ${vaultState.vaultFarm} could not be fetched (invalid or does not exist)`);
}
}
// 3. Check FLC farm is set and valid (warning if not)
if (vaultState.firstLossCapitalFarm === DEFAULT_PUBLIC_KEY) {
result.warnings.push('First loss capital farm is not set');
} else {
const flcFarmState = await FarmState.fetch(this._rpc, vaultState.firstLossCapitalFarm);
if (!flcFarmState) {
result.warnings.push(
`First loss capital farm ${vaultState.firstLossCapitalFarm} could not be fetched (invalid or does not exist)`
);
} else {
if (!(await this.isFlcFarmValid(flcFarmState, vaultState))) {
result.warnings.push(`First loss capital farm ${vaultState.firstLossCapitalFarm} is not valid`);
}
}
}
// 4. Check shares token metadata is set
const [sharesMintMetadata] = await getKVaultSharesMetadataPda(vaultState.sharesMint);
const metadataAccount = await fetchEncodedAccount(this._rpc, sharesMintMetadata, { commitment: 'processed' });
if (!metadataAccount.exists) {
result.errors.push(`Shares token metadata not set for shares mint ${vaultState.sharesMint}`);
}
// 5. Check min deposit is not 0
if (vaultState.minDepositAmount.isZero()) {
result.errors.push('Min deposit amount is 0');
}
// 6. Check the vault has at least one allocation
const activeAllocations = vaultState.vaultAllocationStrategy.filter(
(allocation) => allocation.reserve !== DEFAULT_PUBLIC_KEY
);
if (activeAllocations.length === 0) {
result.errors.push('Vault has no allocations');
}
// 7. Check allocations have weight > 0 and cap > 0, warn if cap != u64::MAX
for (const allocation of activeAllocations) {
if (allocation.targetAllocationWeight.isZero()) {
result.errors.push(`Allocation for reserve ${allocation.reserve} has weight 0`);
}
if (allocation.tokenAllocationCap.isZero()) {
result.errors.push(`Allocation for reserve ${allocation.reserve} has cap 0`);
} else if (allocation.tokenAllocationCap.toString() !== U64_MAX) {
result.warnings.push(
`Allocation for reserve ${
allocation.reserve
} has cap ${allocation.tokenAllocationCap.toString()} (not u64::MAX)`
);
}
}
// 9. Check CDN that the vault admin exists in riskManagers and has a description
const cdnResources = await this.loadCdnResourcesOnce();
if (!cdnResources) {
result.errors.push('Could not fetch CDN resources to verify vault admin');
} else {
const adminEntries = cdnResources.riskManagers[vaultState.vaultAdminAuthority];
if (!adminEntries || adminEntries.length === 0) {
result.errors.push(`Vault admin ${vaultState.vaultAdminAuthority} not found in CDN riskManagers`);
} else {
const hasDescription = adminEntries.some(
(entry: RiskManagerInfo) => entry.description && entry.description.trim().length > 0
);
if (!hasDescription) {
result.errors.push(
`Vault admin ${vaultState.vaultAdminAuthority} found in CDN riskManagers but has no description`
);
}
}
}
result.success = result.errors.length === 0;
return result;
}
/**
* Prints a vault in a human readable form
* @param vaultPubkey - the address of the vault
* @param [vaultState] - optional parameter to pass the vault state directly; this will save a network call
* @param [slot] - optional slot to use for calculations; if not provided, the latest confirmed slot will be fetched
* @returns - void; prints the vault to the console
*/
async printVault(vaultPubkey: Address, vaultState?: VaultState, slot?: Slot) {
const vault = vaultState ? vaultState : await VaultState.fetch(this.getConnection(), vaultPubkey);
if (!vault) {
console.log(`Vault ${vaultPubkey.toString()} not found`);
return;
}
const kaminoVault = KaminoVault.loadWithClientAndState(this, vaultPubkey, vault);
const vaultName = this.decodeVaultName(vault.name);
const currentSlot = slot ?? (await this.getConnection().getSlot({ commitment: 'confirmed' }).send());
const tokensPerShare = await this.getTokensPerShareSingleVault(kaminoVault, currentSlot);
const holdings = await this.getVaultHoldings(vault, currentSlot);
const sharesIssued = new Decimal(vault.sharesIssued.toString()!).div(
new Decimal(vault.sharesMintDecimals.toString())
);
console.log('Name: ', vaultName);
console.log('Shares issued: ', sharesIssued);
holdings.print();
console.log('Tokens per share: ', tokensPerShare);
}
/**
* This method initializes the kvault global config (one off, needs to be signed by program owner)
* @param admin - the admin of the kvault program
* @returns - an instruction to initialize the kvault global config
*/
async initKvaultGlobalConfigIx(admin: TransactionSigner) {
const globalConfigAddress = await getKvaultGlobalConfigPda(this.getProgramID());
const programData = await programDataPda(this.getProgramID());
const ix = initKVaultGlobalConfig(
{
payer: admin,
globalConfig: globalConfigAddress,
programData: programData,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
rent: SYSVAR_RENT_ADDRESS,
},
undefined,
this.getProgramID()
);
return ix;
}
/**
* This method updates the kvault global config
* @param mode - the mode to update the global config with
* @returns - an instruction to update the global config
*/
async updateGlobalConfigIx(mode: string, value: string) {
console.log('in updateGlobalConfigIx');
let modeEnum: UpdateKVaultGlobalConfigModeKind;
switch (mode) {
case 'PendingAdmin': {
// Ensure value is a valid address string before converting
if (!value || value.length < 32) {
throw new Error(`Invalid address value: ${value}`);
}
const addr = address(value);
modeEnum = new UpdateGlobalConfigMode.PendingAdmin([addr]);
break;
}
case 'MinWithdrawalPenaltyLamports': {
modeEnum = new UpdateGlobalConfigMode.MinWithdrawalPenaltyLamports([new BN(value)]);
break;
}
case 'MinWithdrawalPenaltyBPS': {
modeEnum = new UpdateGlobalConfigMode.MinWithdrawalPenaltyBPS([new BN(value)]);
break;
}
default:
throw new Error(`Unknown update mode: ${mode}`);
}
const args: UpdateKVaultGlobalConfigArgs = {
update: modeEnum,
};
const globalConfigAddress = await getKvaultGlobalConfigPda(this.getProgramID());
const globalConfigState = await KVaultGlobalConfig.fetch(this.getConnection(), globalConfigAddress);
if (!globalConfigState) {
throw new Error('Global config not found');
}
const admin = globalConfigState.globalAdmin;
const accounts: UpdateKVaultGlobalConfigAccounts = {
globalAdmin: noopSigner(admin),
globalConfig: globalConfigAddress,
};
return updateKVaultGlobalConfig(args, accounts, undefined, this.getProgramID());
}
/**
* This method accepts the ownership of the global config
* @param admin - the admin of the transaction
* @returns - an instruction to accept the ownership of the global config
*/
async acceptGlobalConfigOwnershipIx(admin: TransactionSigner) {
const globalConfigAddress = await getKvaultGlobalConfigPda(this.getProgramID());
const accounts: UpdateGlobalConfigAdminAccounts = {
pendingAdmin: admin,
globalConfig: globalConfigAddress,
};
return updateGlobalConfigAdmin(accounts, undefined, this.getProgramID());
}
/**
* This method will create a vault with a given config. The config can be changed later on, but it is recommended to set it up correctly from the start
* @param vaultConfig - the config object used to create a vault
* @param [useDevnetFarms] - whether to use devnet farms
* @param [slot] - optional slot to use for lookup table creation; if not provided, the latest finalized slot will be fetched
* @returns vault: the keypair of the vault, used to sign the initialization transaction; initVaultIxs: a struct with ixs to initialize the vault and its lookup table + populateLUTIxs, a list to populate the lookup table which has to be executed in a separate transaction
*/
async createVaultIxs(
vaultConfig: KaminoVaultConfig,
useDevnetFarms: boolean = false,
slot?: Slot
): Promise<{ vault: TransactionSigner; lut: Address; initVaultIxs: InitVaultIxs }> {
const vaultState = await generateKeyPairSigner();
const size = BigInt(VaultState.layout.span + 8);
const createVaultIx = getCreateAccountInstruction({
payer: vaultConfig.admin,
space: size,
lamports: await this.getConnection().getMinimumBalanceForRentExemption(size).send(),
programAddress: this._kaminoVaultProgramId,
newAccount: vaultState,
});
const [resolvedSlot, [tokenVault], [baseVaultAuthority], [sharesMint]] = await Promise.all([
slot ? Promise.resolve(slot) : this.getConnection().getSlot({ commitment: 'finalized' }).send(),
getProgramDerivedAddress({
seeds: [Buffer.from(TOKEN_VAULT_SEED), addressEncoder.encode(vaultState.address)],
programAddress: this._kaminoVaultProgramId,
}),
getProgramDerivedAddress({
seeds: [Buffer.from(BASE_VAULT_AUTHORITY_SEED), addressEncoder.encode(vaultState.address)],
programAddress: this._kaminoVaultProgramId,
}),
getProgramDerivedAddress({
seeds: [Buffer.from(SHARES_SEED), addressEncoder.encode(vaultState.address)],
programAddress: this._kaminoVaultProgramId,
}),
]);
let adminTokenAccount: Address;
const prerequisiteIxs: Instruction[] = [];
const cleanupIxs: Instruction[] = [];
if (vaultConfig.tokenMint === WRAPPED_SOL_MINT) {
const { wsolAta, createAtaIxs, closeAtaIxs } = await createWsolAtaIfMissing(
this.getConnection(),
new Decimal(VAULT_INITIAL_DEPOSIT),
vaultConfig.admin,
vaultConfig.tokenMintProgramId
);
adminTokenAccount = wsolAta;
prerequisiteIxs.push(...createAtaIxs);
cleanupIxs.push(...closeAtaIxs);
} else {
adminTokenAccount = (
await findAssociatedTokenPda({
mint: vaultConfig.tokenMint,
tokenProgram: vaultConfig.tokenMintProgramId,
owner: vaultConfig.admin.address,
})
)[0];
}
const initVaultAccounts: InitVaultAccounts = {
adminAuthority: vaultConfig.admin,
vaultState: vaultState.address,
baseTokenMint: vaultConfig.tokenMint,
tokenVault,
baseVaultAuthority,
sharesMint,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
rent: SYSVAR_RENT_ADDRESS,
tokenProgram: vaultConfig.tokenMintProgramId,
sharesTokenProgram: TOKEN_PROGRAM_ADDRESS,
adminTokenAccount,
};
const initVaultIx = initVault(initVaultAccounts, undefined, this._kaminoVaultProgramId);
const createVaultFarm = await this.createVaultFarm(
vaultConfig.admin,
vaultState.address,
sharesMint,
useDevnetFarms
);
// create and set up the vault lookup table
const [createLUTIx, lut] = await initLookupTableIx(vaultConfig.admin, resolvedSlot);
const farmsGlobalConfig = useDevnetFarms ? FARMS_GLOBAL_CONFIG_DEVNET : FARMS_GLOBAL_CONFIG_MAINNET;
const accountsToBeInserted: Address[] = [
vaultConfig.admin.address,
vaultState.address,
vaultConfig.tokenMint,
vaultConfig.tokenMintProgramId,
baseVaultAuthority,
sharesMint,
SYSTEM_PROGRAM_ADDRESS,
SYSVAR_RENT_ADDRESS,
TOKEN_PROGRAM_ADDRESS,
this._kaminoLendProgramId,
SYSVAR_INSTRUCTIONS_ADDRESS,
createVaultFarm.farm.address,
farmsGlobalConfig,
];
const insertIntoLUTIxs = await insertIntoLookupTableIxs(
this.getConnection(),
vaultConfig.admin,
lut,
accountsToBeInserted,
[]
);
const setLUTIx = await this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.address,
new VaultConfigField.LookupTable(),
lut.toString()
);
const ixs = [createVaultIx, initVaultIx, setLUTIx];
if (vaultConfig.getPerformanceFeeBps() > 0) {
const setPerformanceFeeIx = await this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.address,
new VaultConfigField.PerformanceFeeBps(),
vaultConfig.getPerformanceFeeBps().toString()
);
ixs.push(setPerformanceFeeIx);
}
if (vaultConfig.getManagementFeeBps() > 0) {
const setManagementFeeIx = await this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.address,
new VaultConfigField.ManagementFeeBps(),
vaultConfig.getManagementFeeBps().toString()
);
ixs.push(setManagementFeeIx);
}
if (vaultConfig.name && vaultConfig.name.length > 0) {
const setNameIx = await this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.address,
new VaultConfigField.Name(),
vaultConfig.name
);
ixs.push(setNameIx);
}
const setFarmIx = await this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.address,
new VaultConfigField.Farm(),
createVaultFarm.farm.address
);
const metadataIx = await this.getSetSharesMetadataIx(
this.getConnection(),
vaultConfig.admin,
vaultState.address,
sharesMint,
baseVaultAuthority,
vaultConfig.vaultTokenSymbol,
vaultConfig.vaultTokenName,
undefined,
this._kaminoVaultProgramId
);
return {
vault: vaultState,
lut,
initVaultIxs: {
createAtaIfNeededIxs: prerequisiteIxs,
initVaultIxs: ixs,
createLUTIx,
populateLUTIxs: insertIntoLUTIxs,
cleanupIxs,
initSharesMetadataIx: metadataIx,
createVaultFarm,
setFarmToVaultIx: setFarmIx,
},
};
}
/**
* This method creates a farm for a vault
* @param signer - the signer of the transaction
* @param vaultSharesMint - the mint of the vault shares
* @param vaultAddress - the address of the vault (it doesn't need to be already initialized)
* @returns a struct with the farm, the setup farm ixs and the update farm ixs
*/
async createVaultFarm(
signer: TransactionSigner,
vaultAddress: Address,
vaultSharesMint: Address,
useDevnetFarms: boolean = false
): Promise<CreateVaultFarm> {
const farmsSDK = new Farms(this._rpc, this._farmsProgramId);
const globalConfig = useDevnetFarms ? FARMS_GLOBAL_CONFIG_DEVNET : FARMS_GLOBAL_CONFIG_MAINNET;
const farm = await generateKeyPairSigner();
const ixs = await farmsSDK.createFarmIxs(signer, farm, globalConfig, vaultSharesMint);
const updatePendingFarmAdminIx = await farmsSDK.updateFarmConfigIx(
signer,
farm.address,
DEFAULT_PUBLIC_KEY,
new FarmConfigOption.UpdatePendingFarmAdmin(),
FARMS_ADMIN_MAINNET,
undefined,
undefined,
true
);
const updateFarmVaultIdIx = await farmsSDK.updateFarmConfigIx(
signer,
farm.address,
DEFAULT_PUBLIC_KEY,
new FarmConfigOption.UpdateVaultId(),
vaultAddress,
undefined,
undefined,
true
);
return {
farm,
setupFarmIxs: ixs,
updateFarmIxs: [updatePendingFarmAdminIx, updateFarmVaultIdIx],
};
}
/**
* This method creates an instruction to set the shares metadata for a vault
* @param rpc
* @param vaultAdmin
* @param vault - the vault to set the shares metadata for
* @param sharesMint
* @param baseVaultAuthority
* @param tokenName - the name of the token in the vault (symbol; e.g. "USDC" which becomes "kVUSDC")
* @param extraName - the extra string appended to the prefix("Kamino Vault USDC <extraName>")
* @returns - an instruction to set the shares metadata for the vault
*/
async getSetSharesMetadataIx(
rpc: Rpc<SolanaRpcApi>,
vaultAdmin: TransactionSigner,
vault: Address,
sharesMint: Address,
baseVaultAuthority: Address,
tokenName: string,
extraName: string,
metadataProgramId: Address = METADATA_PROGRAM_ID,
kvaultProgramId?: Address
) {
const kvaultProgramIdToUse = kvaultProgramId ?? this._kaminoVaultProgramId;
const [sharesMintMetadata] = await getKVaultSharesMetadataPda(sharesMint, metadataProgramId);
const { name, symbol, uri } = resolveMetadata(sharesMint, extraName, tokenName);
const ix = !(await fetchEncodedAccount(rpc, sharesMintMetadata, { commitment: 'processed' })).exists
? await getInitializeKVaultSharesMetadataIx(
vaultAdmin,
vault,
sharesMint,
baseVaultAuthority,
name,
symbol,
uri,
metadataProgramId,
kvaultProgramIdToUse
)
: await getUpdateSharesMetadataIx(
vaultAdmin,
vault,
sharesMint,
baseVaultAuthority,
name,
symbol,
uri,
metadataProgramId,
kvaultProgramIdToUse
);
return ix;
}
/**
* This method updates the vault reserve allocation config for an exiting vault reserve, or adds a new reserve to the vault if it does not exist.
* @param vault - vault to be updated
* @param reserveAllocationConfig - new reserve allocation config
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct with an instruction to update the reserve allocation and an optional list of instructions to update the lookup table for the allocation changes
*/
async updateReserveAllocationIxs(
vault: KaminoVault,
reserveAllocationConfig: ReserveAllocationConfig,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateReserveAllocationIxs> {
const vaultState: VaultState = await vault.getState();
const reserveState: Reserve = reserveAllocationConfig.getReserveState();
const cTokenVault = await getCTokenVaultPda(
vault.address,
reserveAllocationConfig.getReserveAddress(),
this._kaminoVaultProgramId
);
const reserveWhitelistEntryOption = await getReserveWhitelistEntryIfExists(
reserveAllocationConfig.getReserveAddress(),
this.getConnection(),
this._kaminoVaultProgramId
);
const vaultAdmin = parseVaultAdmin(vaultState, vaultAdminAuthority);
const updateReserveAllocationAccounts: UpdateReserveAllocationAccounts = {
signer: vaultAdmin,
vaultState: vault.address,
baseVaultAuthority: vaultState.baseVaultAuthority,
reserveCollateralMint: reserveState.collateral.mintPubkey,
reserve: reserveAllocationConfig.getReserveAddress(),
ctokenVault: cTokenVault,
reserveWhitelistEntry: reserveWhitelistEntryOption,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
rent: SYSVAR_RENT_ADDRESS,
reserveCollateralTokenProgram: TOKEN_PROGRAM_ADDRESS,
};
const updateReserveAllocationArgs: UpdateReserveAllocationArgs = {
weight: new BN(reserveAllocationConfig.targetAllocationWeight),
cap: new BN(reserveAllocationConfig.getAllocationCapLamports().floor().toString()),
};
const updateReserveAllocationIx = updateReserveAllocation(
updateReserveAllocationArgs,
updateReserveAllocationAccounts,
undefined,
this._kaminoVaultProgramId
);
const accountsToAddToLut = [
reserveAllocationConfig.getReserveAddress(),
cTokenVault,
...this.getReserveAccountsToInsertInLut(reserveState),
];
const [lendingMarketAuth] = await lendingMarketAuthPda(reserveState.lendingMarket, this._kaminoLendProgramId);
accountsToAddToLut.push(lendingMarketAuth);
const insertIntoLutIxs = await insertIntoLookupTableIxs(
this.getConnection(),
vaultAdmin,
vaultState.vaultLookupTable,
accountsToAddToLut
);
const updateReserveAllocationIxs: UpdateReserveAllocationIxs = {
updateReserveAllocationIx,
updateLUTIxs: insertIntoLutIxs,
};
return updateReserveAllocationIxs;
}
/**
* This method updates the unallocated weight and cap of a vault (both are optional, if not provided the current values will be used)
* @param vault - the vault to update the unallocated weight and cap for
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @param [unallocatedWeight] - the new unallocated weight to set. If not provided, the current unallocated weight will be used
* @param [unallocatedCap] - the new unallocated cap to set. If not provided, the current unallocated cap will be used
* @returns - a list of instructions to update the unallocated weight and cap
*/
async updateVaultUnallocatedWeightAndCapIxs(
vault: KaminoVault,
vaultAdminAuthority?: TransactionSigner,
unallocatedWeight?: BN,
unallocatedCap?: BN
) {
const vaultState = await vault.getState();
const unallocatedWeightToUse = unallocatedWeight ? unallocatedWeight : vaultState.unallocatedWeight;
const unallocatedCapToUse = unallocatedCap ? unallocatedCap : vaultState.unallocatedTokensCap;
const ixs: Instruction[] = [];
if (!unallocatedWeightToUse.eq(vaultState.unallocatedWeight)) {
const updateVaultUnallocatedWeightIx = await this.updateVaultConfigIxs(
vault,
new VaultConfigField.UnallocatedWeight(),
unallocatedWeightToUse.toString(),
vaultAdminAuthority
);
ixs.push(updateVaultUnallocatedWeightIx.updateVaultConfigIx);
}
if (!unallocatedCapToUse.eq(vaultState.unallocatedTokensCap)) {
const updateVaultUnallocatedCapIx = await this.updateVaultConfigIxs(
vault,
new VaultConfigField.UnallocatedTokensCap(),
unallocatedCapToUse.toString(),
vaultAdminAuthority
);
ixs.push(updateVaultUnallocatedCapIx.updateVaultConfigIx);
}
return ixs;
}
/**
* This method withdraws all the funds from a reserve and blocks it from being invested by setting its weight and ctoken allocation to 0
* @param vault - the vault to withdraw the funds from
* @param reserve - the reserve to withdraw the funds from
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct with an instruction to update the reserve allocation and an optional list of instructions to update the lookup table for the allocation changes
*/
async withdrawEverythingAndBlockInvestReserve(
vault: KaminoVault,
reserve: Address,
vaultAdminAuthority?: TransactionSigner
): Promise<WithdrawAndBlockReserveIxs> {
const vaultState = await vault.getState();
const reserveIsPartOfAllocation = vaultState.vaultAllocationStrategy.some(
(allocation) => allocation.reserve === reserve
);
const withdrawAndBlockReserveIxs: WithdrawAndBlockReserveIxs = {
updateReserveAllocationIxs: [],
investIxs: [],
};
if (!reserveIsPartOfAllocation) {
return withdrawAndBlockReserveIxs;
}
const reserveState = await Reserve.fetch(this.getConnection(), reserve);
if (reserveState === null) {
return withdrawAndBlockReserveIxs;
}
const reserveWithAddress: ReserveWithAddress = {
address: reserve,
state: reserveState,
};
const reserveAllocationConfig = new ReserveAllocationConfig(reserveWithAddress, 0, new Decimal(0));
const admin = vaultAdminAuthority ? vaultAdminAuthority : noopSigner(vaultState.vaultAdminAuthority);
// update allocation to have 0 weight and 0 cap
const updateAllocIxs = await this.updateReserveAllocationIxs(vault, reserveAllocationConfig, admin);
const investIx = await this.investSingleReserveIxs(admin, vault, reserveWithAddress);
withdrawAndBlockReserveIxs.updateReserveAllocationIxs = [updateAllocIxs.updateReserveAllocationIx];
withdrawAndBlockReserveIxs.investIxs = investIx;
return withdrawAndBlockReserveIxs;
}
/**
* This method withdraws all the funds from all the reserves and blocks them from being invested by setting their weight and ctoken allocation to 0
* @param vault - the vault to withdraw the invested funds from
* @param [vaultReservesMap] - optional parameter to pass a map of the vault reserves. If not provided, the reserves will be loaded from the vault
* @param [payer] - optional parameter to pass a different payer for the transaction. If not provided, the admin of the vault will be used; this is the payer for the invest ixs and it should have an ATA and some lamports (2x no_of_reserves) of the token vault
* @returns - a struct with an instruction to update the reserve allocations (set weight and ctoken allocation to 0) and an a list of instructions to disinvest the funds in the reserves
*/
async withdrawEverythingFromAllReservesAndBlockInvest(
vault: KaminoVault,
vaultReservesMap?: Map<Address, KaminoReserve>,
payer?: TransactionSigner
): Promise<WithdrawAndBlockReserveIxs> {
const vaultState = await vault.getState();
const reserves = this.getVaultReserves(vaultState);
const withdrawAndBlockReserveIxs: WithdrawAndBlockReserveIxs = {
updateReserveAllocationIxs: [],
investIxs: [],
};
if (!vaultReservesMap) {
vaultReservesMap = await this.loadVaultReserves(vaultState);
}
for (const reserve of reserves) {
const reserveWithAddress: ReserveWithAddress = {
address: reserve,
state: vaultReservesMap.get(reserve)!.state,
};
const reserveAllocationConfig = new ReserveAllocationConfig(reserveWithAddress, 0, new Decimal(0));
// update allocation to have 0 weight and 0 cap
const updateAllocIxs = await this.updateReserveAllocationIxs(vault, reserveAllocationConfig, payer);
withdrawAndBlockReserveIxs.updateReserveAllocationIxs.push(updateAllocIxs.updateReserveAllocationIx);
}
const investPayer = payer ? payer : noopSigner(vaultState.vaultAdminAuthority);
const investIxs = await this.investAllReservesIxs(investPayer, vault, true);
withdrawAndBlockReserveIxs.investIxs = investIxs;
return withdrawAndBlockReserveIxs;
}
/**
* This method disinvests all the funds from all the reserves and set their weight to 0; for vaults that are managed by external bot/crank, the bot can change the weight and invest in the reserves again
* @param vault - the vault to disinvest the invested funds from
* @param [vaultReservesMap] - optional parameter to pass a map of the vault reserves. If not provided, the reserves will be loaded from the vault
* @param [payer] - optional parameter to pass a different payer for the transaction. If not provided, the admin of the vault will be used; this is the payer for the invest ixs and it should have an ATA and some lamports (2x no_of_reserves) of the token vault
* @returns - a struct with an instruction to update the reserve allocations to 0 weight and a list of instructions to disinvest the funds in the reserves
*/
async disinvestAllReservesIxs(
vault: KaminoVault,
vaultReservesMap?: Map<Address, KaminoReserve>,
payer?: TransactionSigner
): Promise<DisinvestAllReservesIxs> {
const vaultState = await vault.getState();
const reserves = this.getVaultReserves(vaultState);
const disinvestAllReservesIxs: DisinvestAllReservesIxs = {
updateReserveAllocationIxs: [],
investIxs: [],
};
if (!vaultReservesMap) {
vaultReservesMap = await this.loadVaultReserves(vaultState);
}
for (const reserve of reserves) {
const reserveWithAddress: ReserveWithAddress = {
address: reserve,
state: vaultReservesMap.get(reserve)!.state,
};
const existingReserveAllocation = vaultState.vaultAllocationStrategy.find(
(allocation) => allocation.reserve === reserve
);
if (!existingReserveAllocation) {
continue;
}
const reserveAllocationConfig = new ReserveAllocationConfig(
reserveWithAddress,
0,
lamportsToDecimal(
new Decimal(existingReserveAllocation.tokenAllocationCap.toString()),
reserveWithAddress.state.liquidity.mintDecimals.toNumber()
)
);
// update allocation to have 0 weight and 0 cap
const updateAllocIxs = await this.updateReserveAllocationIxs(vault, reserveAllocationConfig, payer);
disinvestAllReservesIxs.updateReserveAllocationIxs.push(updateAllocIxs.updateReserveAllocationIx);
}
const investPayer = payer ? payer : noopSigner(vaultState.vaultAdminAuthority);
const investIxs = await this.investAllReservesIxs(investPayer, vault, true);
disinvestAllReservesIxs.investIxs = investIxs;
return disinvestAllReservesIxs;
}
/**
* This method removes a reserve from the vault allocation strategy if already part of the allocation strategy
* @param vault - vault to remove the reserve from
* @param reserve - reserve to remove from the vault allocation strategy
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - an instruction to remove the reserve from the vault allocation strategy or undefined if the reserve is not part of the allocation strategy
*/
async removeReserveFromAllocationIx(
vault: KaminoVault,
reserve: Address,
vaultAdminAuthority?: TransactionSigner
): Promise<Instruction | undefined> {
const vaultState = await vault.getState();
const vaultAdmin = parseVaultAdmin(vaultState, vaultAdminAuthority);
const reserveIsPartOfAllocation = vaultState.vaultAllocationStrategy.some(
(allocation) => allocation.reserve === reserve
);
if (!reserveIsPartOfAllocation) {
return undefined;
}
const accounts: RemoveAllocationAccounts = {
vaultAdminAuthority: vaultAdmin,
vaultState: vault.address,
reserve,
};
return removeAllocation(accounts);
}
/**
* Update a field of the vault. If the field is a pubkey it will return an extra instruction to add that account into the lookup table
* @param vault the vault to update
* @param mode the field to update (based on VaultConfigFieldKind enum)
* @param value the value to update the field with
* @param [adminAuthority] the signer of the transaction. Optional. If not provided the admin of the vault will be used. It should be used when changing the admin of the vault if we want to build or batch multiple ixs in the same tx.
* The global admin should be passed in when wanting to change the AllowAllocationsInWhitelistedReservesOnly or AllowInvestInWhitelistedReservesOnly fields to false
* @param [lutIxsSigner] the signer of the transaction to be used for the lookup table instructions. Optional. If not provided the admin of the vault will be used. It should be used when changing the admin of the vault if we want to build or batch multiple ixs in the same tx
* @param [skipLutUpdate] if true, the lookup table instructions will not be included in the returned instructions
* @param errorOnOverride throw error if vault already has a farm
* @returns a struct that contains the instruction to update the field and an optional list of instructions to update the lookup table
*/
async updateVaultConfigIxs(
vault: KaminoVault,
mode: VaultConfigFieldKind,
value: string,
adminAuthority?: TransactionSigner,
lutIxsSigner?: TransactionSigner,
skipLutUpdate: boolean = false,
errorOnOverride: boolean = true
): Promise<UpdateVaultConfigIxs> {
const vaultState: VaultState = await vault.getState();
const admin = parseVaultAdmin(vaultState, adminAuthority);
const globalConfig = await getKvaultGlobalConfigPda(this._kaminoVaultProgramId);
const updateVaultConfigAccs: UpdateVaultConfigAccounts = {
signer: admin,
globalConfig: globalConfig,
vaultState: vault.address,
klendProgram: this._kaminoLendProgramId,
};
if (mode.kind === new VaultConfigField.Farm().kind) {
if (value != DEFAULT_PUBLIC_KEY && vaultState.vaultFarm != DEFAULT_PUBLIC_KEY) {
if (errorOnOverride) {
throw new Error('Vault already has a farm, if you want to override it set errorOnOverride to false');
}
}
}
const updateVaultConfigArgs: UpdateVaultConfigArgs = {
entry: mode,
data: this.getValueForModeAsBuffer(mode, value),
};
await this.updateVaultConfigValidations(mode, value, vaultState);
const vaultReserves = this.getVaultReserves(vaultState);
const vaultReservesState = await this.loadVaultReserves(vaultState);
let updateVaultConfigIx = updateVaultConfig(
updateVaultConfigArgs,
updateVaultConfigAccs,
undefined,
this._kaminoVaultProgramId
);
updateVaultConfigIx = this.appendRemainingAccountsForVaultReserves(
updateVaultConfigIx,
vaultReserves,
vaultReservesState
);
const updateLUTIxs: Instruction[] = [];
if (!skipLutUpdate) {
const lutIxsSignerAccount = lutIxsSigner ? lutIxsSigner : admin;
if (mode.kind === new VaultConfigField.PendingVaultAdmin().kind) {
const newPubkey = address(value);
const insertIntoLutIxs = await insertIntoLookupTableIxs(
this.getConnection(),
lutIxsSignerAccount,
vaultState.vaultLookupTable,
[newPubkey]
);
updateLUTIxs.push(...insertIntoLutIxs);
} else if (mode.kind === new VaultConfigField.Farm().kind) {
const keysToAddToLUT = [address(value)];
// if the farm already exist we want to read its state to add it to the LUT
try {
const farmState = await FarmState.fetch(this.getConnection(), keysToAddToLUT[0], this._farmsProgramId);
keysToAddToLUT.push(
farmState!.farmVault,
farmState!.farmVaultsAuthority,
farmState!.token.mint,
farmState!.scopePrices,
farmState!.globalConfig
);
const insertIntoLutIxs = await insertIntoLookupTableIxs(
this.getConnection(),
lutIxsSignerAccount,
vaultState.vaultLookupTable,
keysToAddToLUT
);
updateLUTIxs.push(...insertIntoLutIxs);
} catch (error) {
console.log(`Error fetching farm ${keysToAddToLUT[0].toString()} state`, error);
}
}
}
const updateVaultConfigIxs: UpdateVaultConfigIxs = {
updateVaultConfigIx,
updateLUTIxs,
};
return updateVaultConfigIxs;
}
/**
* Update the vault performance fee (in bps).
* @param vault - vault to update
* @param feeBps - performance fee in basis points
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultPerfFeeIxs(
vault: KaminoVault,
feeBps: BN | number | string,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(
vault,
new VaultConfigField.PerformanceFeeBps(),
feeBps.toString(),
vaultAdminAuthority
);
}
/**
* Update the vault management fee (in bps).
* @param vault - vault to update
* @param feeBps - management fee in basis points
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultMgmtFeeIxs(
vault: KaminoVault,
feeBps: BN | number | string,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(
vault,
new VaultConfigField.ManagementFeeBps(),
feeBps.toString(),
vaultAdminAuthority
);
}
/**
* Update the pending admin for the vault (step 1/2 of the ownership transfer).
* @param vault - vault to update
* @param newAdmin - new pending admin pubkey
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @param [lutIxsSigner] - signer for LUT updates when adding the new admin
* @param [skipLutUpdate] - if true, the LUT update instructions are not returned
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultPendingAdminIxs(
vault: KaminoVault,
newAdmin: Address,
vaultAdminAuthority?: TransactionSigner,
lutIxsSigner?: TransactionSigner,
skipLutUpdate: boolean = false
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(
vault,
new VaultConfigField.PendingVaultAdmin(),
newAdmin,
vaultAdminAuthority,
lutIxsSigner,
skipLutUpdate
);
}
/**
* Update the vault name.
* @param vault - vault to update
* @param name - new vault name
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultNameIxs(
vault: KaminoVault,
name: string,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(vault, new VaultConfigField.Name(), name, vaultAdminAuthority);
}
/**
* Update the vault lookup table address.
* @param vault - vault to update
* @param lookupTable - new LUT address
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultLookupTableIxs(
vault: KaminoVault,
lookupTable: Address,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(vault, new VaultConfigField.LookupTable(), lookupTable, vaultAdminAuthority);
}
/**
* Update the vault allocation admin.
* @param vault - vault to update
* @param allocationAdmin - new allocation admin pubkey
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultAllocationAdminIxs(
vault: KaminoVault,
allocationAdmin: Address,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(
vault,
new VaultConfigField.AllocationAdmin(),
allocationAdmin,
vaultAdminAuthority
);
}
/**
* Update the vault unallocated weight.
* @param vault - vault to update
* @param unallocatedWeight - new unallocated weight
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT updates
*/
async updateVaultUnallocatedWeightIxs(
vault: KaminoVault,
unallocatedWeight: BN | number | string,
vaultAdminAuthority?: TransactionSigner
): Promise<UpdateVaultConfigIxs> {
return this.updateVaultConfigIxs(
vault,
new VaultConfigField.UnallocatedWeight(),
unallocatedWeight.toString(),
vaultAdminAuthority
);
}
/**
* Update the vault unallocated tokens cap.
* @param vault - vault to update
* @param unallocatedTokensCap - new unallocated tokens cap
* @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs
* @returns - a struct containing the update instruction and optional LUT