use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use super::manager::VaultIx;
use super::{VaultChainError, VaultManager, pda};
use crate::VaultDelegatePermissions;
use crate::chain::ix::{
ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, derive_associated_token_address as ata,
};
const CAPABILITIES_KNOWN_MASK: u32 = (1 << 3) - 1;
const MAX_REBALANCE_DRIFT_BPS: u16 = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VaultPolicyUpdate {
pub max_open_positions: u16,
pub max_rebalance_drift_bps: u16,
pub min_main_liquidity_bps: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VaultGovernanceUpdate {
CapLimit(u64),
Fees {
performance_bps: u16,
management_bps: u16,
},
HwmReset(u128),
Capabilities {
value: u32,
mask: u32,
},
Policy(VaultPolicyUpdate),
GovernanceAuthority(Pubkey),
SecondLeg(Pubkey),
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VaultGovernanceUpdateKind {
CapLimit = 0,
Fees = 1,
HwmReset = 2,
Capabilities = 3,
Policy = 4,
GovernanceAuthority = 5,
SecondLeg = 6,
}
impl VaultGovernanceUpdate {
#[must_use]
pub const fn kind(self) -> VaultGovernanceUpdateKind {
match self {
Self::CapLimit(_) => VaultGovernanceUpdateKind::CapLimit,
Self::Fees { .. } => VaultGovernanceUpdateKind::Fees,
Self::HwmReset(_) => VaultGovernanceUpdateKind::HwmReset,
Self::Capabilities { .. } => VaultGovernanceUpdateKind::Capabilities,
Self::Policy(_) => VaultGovernanceUpdateKind::Policy,
Self::GovernanceAuthority(_) => VaultGovernanceUpdateKind::GovernanceAuthority,
Self::SecondLeg(_) => VaultGovernanceUpdateKind::SecondLeg,
}
}
pub fn payload(self) -> Result<[u8; 32], VaultChainError> {
let mut payload = [0u8; 32];
match self {
Self::CapLimit(value) => payload[..8].copy_from_slice(&value.to_le_bytes()),
Self::HwmReset(value) => payload[..16].copy_from_slice(&value.to_le_bytes()),
Self::Fees {
performance_bps,
management_bps,
} => {
if performance_bps > 10_000 || management_bps > 1_000 {
return Err(VaultChainError::InvalidInput(
"fees exceed 10000 bps performance or 1000 bps management",
));
}
payload[..2].copy_from_slice(&performance_bps.to_le_bytes());
payload[2..4].copy_from_slice(&management_bps.to_le_bytes());
}
Self::Capabilities { value, mask } => {
if mask == 0 || value & !mask != 0 || mask & !CAPABILITIES_KNOWN_MASK != 0 {
return Err(VaultChainError::InvalidInput(
"capability mask must be non-zero and contain every value bit",
));
}
payload[..4].copy_from_slice(&value.to_le_bytes());
payload[4..8].copy_from_slice(&mask.to_le_bytes());
}
Self::Policy(policy) => {
if policy.max_open_positions == 0
|| policy.max_rebalance_drift_bps == 0
|| policy.max_rebalance_drift_bps > MAX_REBALANCE_DRIFT_BPS
|| policy.min_main_liquidity_bps > 10_000
{
return Err(VaultChainError::InvalidInput(
"vault policy requires positions > 0, drift in 1..=200 bps and liquidity <= 10000 bps",
));
}
payload[..2].copy_from_slice(&policy.max_open_positions.to_le_bytes());
payload[2..4].copy_from_slice(&policy.max_rebalance_drift_bps.to_le_bytes());
payload[4..6].copy_from_slice(&policy.min_main_liquidity_bps.to_le_bytes());
}
Self::GovernanceAuthority(key) | Self::SecondLeg(key) => {
if key == Pubkey::default() {
return Err(VaultChainError::InvalidInput(
"vault authority or second-leg update cannot use the zero pubkey",
));
}
payload.copy_from_slice(key.as_ref());
}
}
Ok(payload)
}
}
impl VaultManager {
pub fn propose_update(
&self,
update: VaultGovernanceUpdate,
) -> Result<Instruction, VaultChainError> {
let kind = update.kind();
let mut args = [0u8; 33];
args[0] = kind as u8;
args[1..].copy_from_slice(&update.payload()?);
Ok(self.instruction(
VaultIx::ProposeVaultUpdate,
vec![
AccountMeta::new(self.args.actor, true),
AccountMeta::new(self.addresses.vault, false),
AccountMeta::new(self.pending_update(kind), false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(self.addresses.global_config, false),
],
&args,
))
}
pub fn apply_update(
&self,
kind: VaultGovernanceUpdateKind,
proposal_payer: Pubkey,
price_section: Option<&[AccountMeta]>,
) -> Result<Instruction, VaultChainError> {
if kind == VaultGovernanceUpdateKind::SecondLeg {
return Err(VaultChainError::InvalidInput(
"second-leg updates apply through apply_second_leg",
));
}
let mut ix = self.instruction(
VaultIx::ApplyVaultUpdate,
vec![
AccountMeta::new(self.addresses.vault, false),
AccountMeta::new(self.addresses.policy, false),
AccountMeta::new(self.pending_update(kind), false),
AccountMeta::new(proposal_payer, false),
],
&[kind as u8],
);
if matches!(
kind,
VaultGovernanceUpdateKind::Fees | VaultGovernanceUpdateKind::HwmReset
) {
let Some(price_section) = price_section else {
return Err(VaultChainError::InvalidInput(
"fee and HWM updates require the price section to settle the old terms",
));
};
let mut equity = self.process_equity(price_section).accounts;
equity.drain(1..4);
ix.accounts.extend(equity);
}
Ok(ix)
}
pub fn apply_second_leg(
&self,
proposal_payer: Pubkey,
new_mint: Pubkey,
price_section: Option<&[AccountMeta]>,
) -> Result<Instruction, VaultChainError> {
let args = &self.args;
if [
args.main_mint,
args.second_mint,
args.share_mint,
Pubkey::default(),
]
.contains(&new_mint)
{
return Err(VaultChainError::InvalidInput(
"new second mint must differ from the vault's mints",
));
}
let kind = VaultGovernanceUpdateKind::SecondLeg;
let a = &self.addresses;
let mut accounts = vec![
AccountMeta::new(a.vault, false),
AccountMeta::new(a.policy, false),
AccountMeta::new(self.pending_update(kind), false),
AccountMeta::new(proposal_payer, false),
AccountMeta::new(args.actor, true),
AccountMeta::new(args.second_mint, false),
AccountMeta::new(a.vault_second_ata, false),
AccountMeta::new_readonly(args.second_token_program, false),
AccountMeta::new_readonly(new_mint, false),
AccountMeta::new(ata(&a.vault, &new_mint, &args.second_token_program), false),
AccountMeta::new_readonly(args.second_token_program, false),
AccountMeta::new_readonly(pda::price_route(&args.program_id, &new_mint).0, false),
AccountMeta::new_readonly(a.second_price_route, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(a.vault_main_ata, false),
];
accounts.extend(price_section.into_iter().flatten().cloned());
Ok(self.instruction(VaultIx::ApplyVaultUpdate, accounts, &[kind as u8]))
}
#[must_use]
pub fn cancel_update(
&self,
kind: VaultGovernanceUpdateKind,
proposal_payer: Pubkey,
) -> Instruction {
self.instruction(
VaultIx::CancelVaultUpdate,
vec![
AccountMeta::new_readonly(self.args.actor, true),
AccountMeta::new(self.addresses.vault, false),
AccountMeta::new(self.pending_update(kind), false),
AccountMeta::new(proposal_payer, false),
],
&[kind as u8],
)
}
pub fn rotate_delegate(
&self,
new_delegate: Pubkey,
permissions: VaultDelegatePermissions,
expires_at: u64,
) -> Result<Instruction, VaultChainError> {
let bits = permissions.bits();
if new_delegate == Pubkey::default()
|| bits == 0
|| bits & !VaultDelegatePermissions::KNOWN_MASK != 0
{
return Err(VaultChainError::InvalidInput(
"new delegate must be non-zero and have known permissions",
));
}
let vault = &self.addresses.vault;
let new_account = pda::vault_delegate(&self.args.program_id, vault, &new_delegate).0;
let mut args = Vec::with_capacity(44);
args.extend_from_slice(new_delegate.as_ref());
args.extend_from_slice(&bits.to_le_bytes());
args.extend_from_slice(&expires_at.to_le_bytes());
Ok(self.instruction(
VaultIx::RotateVaultDelegate,
vec![
AccountMeta::new(self.args.actor, true),
AccountMeta::new(*vault, false),
AccountMeta::new(self.addresses.delegate, false),
AccountMeta::new(new_account, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
],
&args,
))
}
#[must_use]
pub fn disable_delegate(&self) -> Instruction {
self.instruction(
VaultIx::DisableVaultDelegate,
vec![
AccountMeta::new_readonly(self.args.actor, true),
AccountMeta::new_readonly(self.addresses.vault, false),
AccountMeta::new(self.addresses.delegate, false),
],
&[],
)
}
pub(super) fn pending_update(&self, kind: VaultGovernanceUpdateKind) -> Pubkey {
pda::pending_vault_update(&self.args.program_id, &self.addresses.vault, kind as u8).0
}
}