From 71181fbe6294b088e47407a93495d7f3a9696f91 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 13 Jan 2025 17:38:45 -0800 Subject: [PATCH] initial commit with account, aum, swaps --- programs/drift/src/error.rs | 8 + programs/drift/src/state/mod.rs | 3 + programs/drift/src/state/vault.rs | 482 ++++++++++++++++++ programs/drift/src/state/vault_constituent.rs | 87 ++++ .../drift/src/state/vault_constituent_map.rs | 152 ++++++ 5 files changed, 732 insertions(+) create mode 100644 programs/drift/src/state/vault.rs create mode 100644 programs/drift/src/state/vault_constituent.rs create mode 100644 programs/drift/src/state/vault_constituent_map.rs diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 5c2d229ab3..60f1409fd6 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -621,6 +621,14 @@ pub enum ErrorCode { InvalidPythLazerMessage, #[msg("Pyth lazer message does not correspond to correct fed id")] PythLazerMessagePriceFeedMismatch, + #[msg("InvalidLiquidateSpotWithSwap")] + InvalidLiquidateSpotWithSwap, + #[msg("Unable to load Constituent")] + UnableToLoadConstituent, + #[msg("Constituent wrong mutability")] + ConstituentWrongMutability, + #[msg("Constituent not found")] + ConstituentNotFound, } #[macro_export] diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index c3549169b3..e0ed61ac3d 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -25,3 +25,6 @@ pub mod swift_user; pub mod traits; pub mod user; pub mod user_map; +pub mod vault; +pub mod vault_constituent; +pub mod vault_constituent_map; \ No newline at end of file diff --git a/programs/drift/src/state/vault.rs b/programs/drift/src/state/vault.rs new file mode 100644 index 0000000000..c8eac3a716 --- /dev/null +++ b/programs/drift/src/state/vault.rs @@ -0,0 +1,482 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + +use anchor_lang::prelude::*; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, SPOT_WEIGHT_PRECISION_U128, +}; +#[cfg(test)] +use crate::math::constants::{PRICE_PRECISION_I64, SPOT_CUMULATIVE_INTEREST_PRECISION}; +use crate::math::margin::{ + calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, + MarginRequirementType, +}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::{calculate_utilization, get_token_amount, get_token_value}; + +use crate::math::stats::calculate_new_twap; +use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSource}; +use crate::state::paused_operations::{InsuranceFundOperation, SpotOperation}; +use crate::state::perp_market::{MarketStatus, PoolBalance}; +use crate::state::traits::{MarketIndexOffset, Size}; +use crate::{validate, PERCENTAGE_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64}; + +use crate::state::oracle_map::OracleMap; +use crate::state::vault_constituent_map::VaultConstituentMap; + +use super::vault_constituent::Constituent; + +#[account] +#[derive(PartialEq, Eq, Debug)] +pub struct Vault { + /// address of the vault. + pub pubkey: Pubkey, + // vault token mint + pub mint: Pubkey, + /// vault token token account + pub token_vault: Pubkey, + /// the constituents of the vault. + pub constituents: Vec, + /// vault token supply + pub token_supply: u64, + /// AUM of the vault in USD, updated lazily + pub last_aum: u128, + /// timestamp of last AUM update + pub last_aum_ts: i64, +} + +impl Default for Vault { + fn default() -> Self { + Vault { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + token_vault: Pubkey::default(), + constituents: vec![], + token_supply: 0, + last_aum: 0, + last_aum_ts: 0, + } + } +} + +impl Vault { + pub fn update_aum_and_weights( + &mut self, + constituents: &VaultConstituentMap, + oracle_map: &mut OracleMap, + clock: &Clock, + ) -> DriftResult<(u128, Vec)> { + let mut aum = 0; + let mut weights = vec![]; + + for constituent_key in self.constituents.iter() { + let constituent = constituents.get_ref(constituent_key)?; + let oracle_price_data = + oracle_map.get_price_data(&(constituent.oracle, constituent.oracle_source))?; + let token_precision = 10_u128.pow(constituent.decimals as u32); + + // decimals * price + aum = aum.safe_add( + constituent + .deposit_balance + .safe_mul(oracle_price_data.price.cast()?)? + .safe_div(token_precision)?, + )?; + } + + for constituent_key in self.constituents.iter() { + let constituent = constituents.get_ref(constituent_key)?; + let oracle_price_data = + oracle_map.get_price_data(&(constituent.oracle, constituent.oracle_source))?; + let token_precision = 10_u128.pow(constituent.decimals as u32); + + weights.push( + constituent + .deposit_balance + .safe_mul(oracle_price_data.price.cast()?)? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(token_precision)? + .safe_div(aum)?, + ) + } + + msg!("aum: {}, weights: {:?}", aum, weights); + self.last_aum = aum; + self.last_aum_ts = clock.unix_timestamp; + + Ok((aum, weights)) + } + + /// get nav of the vault + /// returns NAV in PRICE_PRECISION + pub fn get_nav(&self) -> DriftResult { + if self.token_supply == 0 { + return Ok(0); + } + + self.last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(self.token_supply as u128) + } + + /// get the swap price between two constituents. A `None` constituent is assumed to be the vault token. + /// returns swap price in PRICE_PRECISION + pub fn get_swap_price( + &self, + constituents: &VaultConstituentMap, + oracle_map: &mut OracleMap, + in_constituent: Option, + out_constituent: Option, + in_amount: u128, + ) -> DriftResult { + + validate!( + in_constituent.is_some() || out_constituent.is_some(), + ErrorCode::DefaultError, + "in_constituent and out_constituent cannot both be None" + )?; + + let get_token_constituent = move |key: Pubkey| -> (Option, u32) { + let c = constituents + .get_ref(&key) + .expect("failed to get constituent"); + (Some(*c), c.decimals) + }; + let get_vault_token_constituent = || -> (Option, u32) { (None, 6) }; + + let (in_constituent, in_decimals) = + in_constituent.map_or_else(&get_vault_token_constituent, &get_token_constituent); + let (out_constituent, out_decimals) = + out_constituent.map_or_else(&get_vault_token_constituent, &get_token_constituent); + + let in_price = in_constituent.map_or_else( + || self.get_nav().expect("failed to get nav"), + |c| { + oracle_map + .get_price_data(&(c.oracle, c.oracle_source)) + .expect("failed to get price data") + .price + .cast() + .expect("failed to cast price") + }, + ); + + let out_price = out_constituent.map_or_else( + || self.get_nav().expect("failed to get nav"), + |c| { + oracle_map + .get_price_data(&(c.oracle, c.oracle_source)) + .expect("failed to get price data") + .price + .cast() + .expect("failed to cast price") + }, + ); + + let (prec_diff_numerator, prec_diff_denominator) = if out_decimals > in_decimals { + (10_u128.pow(out_decimals - in_decimals), 1) + } else { + (1, 10_u128.pow(in_decimals - out_decimals)) + }; + + let swap_price = in_amount + .safe_mul(in_price)? + .safe_mul(prec_diff_numerator)? + .safe_div(out_price.safe_mul(prec_diff_denominator)?)?; + + Ok(swap_price) + } + + pub fn swap( + &mut self, + in_constituent: Option, + out_constituent: Option, + in_amount: u128, + ) -> DriftResult { + Ok(0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::vault_constituent::Constituent; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, create_anchor_account_info}; + use crate::{test_utils::*, PRICE_PRECISION}; + use anchor_lang::prelude::Pubkey; + use std::str::FromStr; + + #[test] + fn test_update_aum_and_weights() { + let sol_account_key = + Pubkey::from_str("B3VkEqUtGPMPu95iLVifzrtfzKQsrEy1trYrwLCRFQ6m").unwrap(); + let sol_oracle_key = + Pubkey::from_str("BAtFj4kQttZRVep3UZS2aZRDixkGYgWsbqTBVDbnSsPF").unwrap(); + let btc_account_key = + Pubkey::from_str("9nnLbotNTcUhvbrsA6Mdkx45Sm82G35zo28AqUvjExn8").unwrap(); + let btc_oracle_key = + Pubkey::from_str("486kr3pmFPfTsS4aZgcsQ7kS4i9rjMsYYZup6HQNSTT4").unwrap(); + + let mut vault = Vault { + constituents: vec![sol_account_key, btc_account_key], + ..Default::default() + }; + + let oracle_program = crate::ids::pyth_program::id(); + create_account_info!( + get_pyth_price(200, 6), + &sol_oracle_key, + &oracle_program, + sol_oracle_account_info + ); + create_account_info!( + get_pyth_price(100_000, 6), + &btc_oracle_key, + &oracle_program, + btc_oracle_account_info + ); + + let mut oracle_map = OracleMap::load( + &mut vec![sol_oracle_account_info, btc_oracle_account_info] + .iter() + .peekable(), + 0, + None, + ) + .expect("failed to load oracle map"); + + create_anchor_account_info!( + Constituent { + pubkey: sol_account_key, + oracle: sol_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 500_000_000_000, + decimals: 9, + ..Default::default() + }, + &sol_account_key, + Constituent, + sol_constituent_account_info + ); + create_anchor_account_info!( + Constituent { + pubkey: btc_account_key, + oracle: btc_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 100_000_000, + decimals: 8, + ..Default::default() + }, + &btc_account_key, + Constituent, + btc_constituent_account_info + ); + + let constituents = VaultConstituentMap::load_multiple(vec![ + &sol_constituent_account_info, + &btc_constituent_account_info, + ]) + .expect("failed to load constituents"); + + let (aum, weights) = vault + .update_aum_and_weights(&constituents, &mut oracle_map, &Clock::default()) + .expect("failed to update aum and weights"); + + assert_eq!(aum, 200_000 * PRICE_PRECISION); + assert_eq!( + weights, + vec![ + 50 * PERCENTAGE_PRECISION / 100, + 50 * PERCENTAGE_PRECISION / 100 + ] + ); + } + + #[test] + fn test_get_nav() { + let sol_account_key = Pubkey::from_str("B3VkEqUtGPMPu95iLVifzrtfzKQsrEy1trYrwLCRFQ6m").unwrap(); + let sol_oracle_key = Pubkey::from_str("BAtFj4kQttZRVep3UZS2aZRDixkGYgWsbqTBVDbnSsPF").unwrap(); + let btc_account_key = Pubkey::from_str("9nnLbotNTcUhvbrsA6Mdkx45Sm82G35zo28AqUvjExn8").unwrap(); + let btc_oracle_key = Pubkey::from_str("486kr3pmFPfTsS4aZgcsQ7kS4i9rjMsYYZup6HQNSTT4").unwrap(); + + let vault = Vault { + constituents: vec![sol_account_key, btc_account_key], + token_supply: 100_000_000_000, + last_aum: 200_000 * PRICE_PRECISION, + ..Default::default() + }; + + let oracle_program = crate::ids::pyth_program::id(); + create_account_info!( + get_pyth_price(200, 6), + &sol_oracle_key, + &oracle_program, + sol_oracle_account_info + ); + create_account_info!( + get_pyth_price(100_000, 6), + &btc_oracle_key, + &oracle_program, + btc_oracle_account_info + ); + + let mut oracle_map = OracleMap::load( + &mut vec![sol_oracle_account_info, btc_oracle_account_info] + .iter() + .peekable(), + 0, + None, + ) + .expect("failed to load oracle map"); + + create_anchor_account_info!( + Constituent { + pubkey: sol_account_key, + oracle: sol_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 500_000_000_000, + decimals: 9, + ..Default::default() + }, + &sol_account_key, + Constituent, + sol_constituent_account_info + ); + create_anchor_account_info!( + Constituent { + pubkey: btc_account_key, + oracle: btc_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 100_000_000, + decimals: 8, + ..Default::default() + }, + &btc_account_key, + Constituent, + btc_constituent_account_info + ); + + let constituents = VaultConstituentMap::load_multiple(vec![ + &sol_constituent_account_info, + &btc_constituent_account_info, + ]) + .expect("failed to load constituents"); + + let nav = vault + .get_nav() + .expect("failed to get nav"); + + assert_eq!(nav, 2 * PRICE_PRECISION); + } + + #[test] + fn test_get_swap_price() { + let sol_account_key = + Pubkey::from_str("B3VkEqUtGPMPu95iLVifzrtfzKQsrEy1trYrwLCRFQ6m").unwrap(); + let sol_oracle_key = + Pubkey::from_str("BAtFj4kQttZRVep3UZS2aZRDixkGYgWsbqTBVDbnSsPF").unwrap(); + let btc_account_key = + Pubkey::from_str("9nnLbotNTcUhvbrsA6Mdkx45Sm82G35zo28AqUvjExn8").unwrap(); + let btc_oracle_key = + Pubkey::from_str("486kr3pmFPfTsS4aZgcsQ7kS4i9rjMsYYZup6HQNSTT4").unwrap(); + + let vault = Vault { + constituents: vec![sol_account_key, btc_account_key], + token_supply: 100_000_000_000, + last_aum: 200_000 * PRICE_PRECISION, + ..Default::default() + }; + + let oracle_program = crate::ids::pyth_program::id(); + create_account_info!( + get_pyth_price(200, 6), + &sol_oracle_key, + &oracle_program, + sol_oracle_account_info + ); + create_account_info!( + get_pyth_price(100_000, 6), + &btc_oracle_key, + &oracle_program, + btc_oracle_account_info + ); + + let mut oracle_map = OracleMap::load( + &mut vec![sol_oracle_account_info, btc_oracle_account_info] + .iter() + .peekable(), + 0, + None, + ) + .expect("failed to load oracle map"); + + create_anchor_account_info!( + Constituent { + pubkey: sol_account_key, + oracle: sol_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 500_000_000_000, + decimals: 9, + ..Default::default() + }, + &sol_account_key, + Constituent, + sol_constituent_account_info + ); + create_anchor_account_info!( + Constituent { + pubkey: btc_account_key, + oracle: btc_oracle_key, + oracle_source: OracleSource::Pyth, + deposit_balance: 100_000_000, + decimals: 8, + ..Default::default() + }, + &btc_account_key, + Constituent, + btc_constituent_account_info + ); + + let constituents = VaultConstituentMap::load_multiple(vec![ + &sol_constituent_account_info, + &btc_constituent_account_info, + ]) + .expect("failed to load constituents"); + + // Test SOL -> BTC swap price + let out_amount = vault + .get_swap_price( + &constituents, + &mut oracle_map, + Some(sol_account_key), + Some(btc_account_key), + 1_000_000_000, // 1 SOL + &Clock::default(), + ) + .expect("failed to get swap price"); + + // 1 SOL = $200/$100k = 0.002 BTC + assert_eq!(out_amount, 200_000); + + // Test BTC -> Vault token swap price + let out_amount = vault + .get_swap_price( + &constituents, + &mut oracle_map, + Some(btc_account_key), + Some(sol_account_key), + 100_000_000, // 1 BTC + &Clock::default(), + ) + .expect("failed to get swap price"); + + // 1 BTC = $100k/$200 = 500 SOL + assert_eq!(out_amount, 500_000_000_000); + } +} diff --git a/programs/drift/src/state/vault_constituent.rs b/programs/drift/src/state/vault_constituent.rs new file mode 100644 index 0000000000..a21ff2380a --- /dev/null +++ b/programs/drift/src/state/vault_constituent.rs @@ -0,0 +1,87 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, SPOT_WEIGHT_PRECISION_U128, +}; +#[cfg(test)] +use crate::math::constants::{PRICE_PRECISION_I64, SPOT_CUMULATIVE_INTEREST_PRECISION}; +use crate::math::margin::{ + calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, + MarginRequirementType, +}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::{calculate_utilization, get_token_amount, get_token_value}; + +use crate::math::stats::calculate_new_twap; +use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSource}; +use crate::state::paused_operations::{InsuranceFundOperation, SpotOperation}; +use crate::state::perp_market::{MarketStatus, PoolBalance}; +use crate::state::traits::{MarketIndexOffset, Size}; +use crate::{validate, PERCENTAGE_PRECISION}; + +use super::oracle::OraclePriceData; +use super::oracle_map::OracleIdentifier; + +#[account(zero_copy(unsafe))] +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct Constituent { + /// address of the constituent + pub pubkey: Pubkey, + /// oracle used to price the constituent + pub oracle: Pubkey, + pub oracle_source: OracleSource, + /// target weight of the constituent in the Vault + /// precision: PERCENTAGE_PRECISION + pub target_weight: u64, + /// max deviation from target_weight allowed for the constituent + /// precision: PERCENTAGE_PRECISION + pub max_weight_deviation: u64, + /// min fee charged on swaps to this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_min: u64, + /// max premium to be applied to swap_fee_min when the constituent is at max deviation from target_weight + /// precision: PERCENTAGE_PRECISION + pub max_fee_premium: u64, + /// underlying drift spot market index + pub spot_market_index: u16, + /// oracle price at last update + /// precision: PRICE_PRECISION_I64 + pub last_oracle_price: i64, + /// timestamp of last oracle price update: + pub last_oracle_price_ts: u64, + /// deposit balance of the constituent in token precision + /// precision: SPOT_BALANCE_PRECISION + pub deposit_balance: u128, + /// decimals of the constituent mint + pub decimals: u32 +} + +impl Size for Constituent { + const SIZE: usize = 152; +} + +impl Default for Constituent { + fn default() -> Self { + Constituent { + pubkey: Pubkey::default(), + oracle: Pubkey::default(), + oracle_source: OracleSource::default(), + target_weight: 0, + max_weight_deviation: 0, + swap_fee_min: 0, + max_fee_premium: 0, + spot_market_index: 0, + last_oracle_price: 0, + last_oracle_price_ts: 0, + deposit_balance: 0, + decimals: 0, + } + } +} diff --git a/programs/drift/src/state/vault_constituent_map.rs b/programs/drift/src/state/vault_constituent_map.rs new file mode 100644 index 0000000000..d74d42bbcf --- /dev/null +++ b/programs/drift/src/state/vault_constituent_map.rs @@ -0,0 +1,152 @@ +use anchor_lang::accounts::account_loader::AccountLoader; +use anchor_lang::{Discriminator, Key}; +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use arrayref::array_ref; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::Peekable; +use std::panic::Location; +use std::cell::{Ref, RefMut}; +use std::slice::Iter; + +use solana_program::msg; + +use crate::error::{DriftResult, ErrorCode}; +use crate::state::vault_constituent::Constituent; +use crate::state::vault::Vault; +use crate::state::traits::Size; + +pub struct VaultConstituentMap<'a>(pub BTreeMap>); + +impl<'a> VaultConstituentMap<'a> { + #[track_caller] + #[inline(always)] + pub fn get_ref(&self, key: &Pubkey) -> DriftResult> { + let loader = match self.0.get(key) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + key, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load() { + Ok(constituent) => Ok(constituent), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + key, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadConstituent) + } + } + } + + pub fn load<'b, 'c>( + vault: &Vault, + writable_constituents: &'b ConstituentSet, + account_info_iter: &'c mut Peekable>>, + ) -> DriftResult> { + let mut vault_constituent_map = VaultConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + while let Some(account_info) = account_info_iter.peek() { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::UnableToLoadConstituent))?; + + msg!("loading constituent {}", account_info.key()); + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + break; + } + + if !vault.constituents.contains(&account_info.key()) { + break; + } + + let is_writable = account_info.is_writable; + if writable_constituents.contains(&account_info.key()) && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + if vault_constituent_map.0.contains_key(&account_info.key()) { + msg!("Can not include same constituent twice {}", account_info.key()); + return Err(ErrorCode::UnableToLoadConstituent); + } + + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::UnableToLoadConstituent))?; + msg!("Loaded constituent {}", account_info.key()); + + vault_constituent_map.0.insert(account_info.key(), account_loader); + } + + Ok(vault_constituent_map) + } +} + +#[cfg(test)] +impl<'a> VaultConstituentMap<'a> { + pub fn load_multiple<'c: 'a>( + account_info: Vec<&'c AccountInfo<'a>>, + ) -> DriftResult> { + let mut vault_constituent_map = VaultConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let account_info_iter = account_info.into_iter(); + for account_info in account_info_iter { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::UnableToLoadConstituent))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + break; + } + + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::UnableToLoadConstituent))?; + + vault_constituent_map.0.insert(account_info.key(), account_loader); + } + + Ok(vault_constituent_map) + } +} + +pub(crate) type ConstituentSet = BTreeSet; + +pub fn get_writable_constituent_set(constituent_index: Pubkey) -> ConstituentSet { + let mut writable_constituents = ConstituentSet::new(); + writable_constituents.insert(constituent_index); + writable_constituents +} + +pub fn get_writable_constituent_set_from_vec(constituent_indexes: &[Pubkey]) -> ConstituentSet { + let mut writable_constituents = ConstituentSet::new(); + for constituent_index in constituent_indexes.iter() { + writable_constituents.insert(*constituent_index); + } + writable_constituents +}