diff --git a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs index 924dad343..824c637ef 100644 --- a/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs +++ b/programs/futarchy/src/instructions/admin_approve_execute_multisig_proposal.rs @@ -77,7 +77,7 @@ impl<'info, 'c: 'info> AdminApproveExecuteMultisigProposal<'info> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; // Approve the proposal diff --git a/programs/futarchy/src/instructions/collect_fees.rs b/programs/futarchy/src/instructions/collect_fees.rs index 68b8ae3a4..7dbf667cf 100644 --- a/programs/futarchy/src/instructions/collect_fees.rs +++ b/programs/futarchy/src/instructions/collect_fees.rs @@ -17,7 +17,7 @@ pub mod metadao_admin { #[event_cpi] pub struct CollectFees<'info> { #[account(mut)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub admin: Signer<'info>, #[account(mut, associated_token::mint = dao.base_mint, associated_token::authority = metadao_multisig_vault::ID)] pub base_token_account: Account<'info, TokenAccount>, @@ -68,7 +68,7 @@ impl CollectFees<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs index 93ce666c2..3d8c387ce 100644 --- a/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs +++ b/programs/futarchy/src/instructions/collect_meteora_damm_fees.rs @@ -238,7 +238,7 @@ impl CollectMeteoraDammFees<'_> { let dao_nonce = &ctx.accounts.dao.nonce.to_le_bytes(); let dao_creator_key = ctx.accounts.dao.dao_creator.as_ref(); let dao_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator_key, dao_nonce, &[ctx.accounts.dao.pda_bump], diff --git a/programs/futarchy/src/instructions/conditional_swap.rs b/programs/futarchy/src/instructions/conditional_swap.rs index 881786da6..06c04b9e9 100644 --- a/programs/futarchy/src/instructions/conditional_swap.rs +++ b/programs/futarchy/src/instructions/conditional_swap.rs @@ -166,7 +166,7 @@ impl ConditionalSwap<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/instructions/execute_spending_limit_change.rs b/programs/futarchy/src/instructions/execute_spending_limit_change.rs index 5f46afdb6..644f84d88 100644 --- a/programs/futarchy/src/instructions/execute_spending_limit_change.rs +++ b/programs/futarchy/src/instructions/execute_spending_limit_change.rs @@ -73,7 +73,7 @@ impl<'info, 'c: 'info> ExecuteSpendingLimitChange<'info> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; squads_multisig_program::cpi::vault_transaction_execute( diff --git a/programs/futarchy/src/instructions/finalize_proposal.rs b/programs/futarchy/src/instructions/finalize_proposal.rs index 1abf2eb8a..aeacfe146 100644 --- a/programs/futarchy/src/instructions/finalize_proposal.rs +++ b/programs/futarchy/src/instructions/finalize_proposal.rs @@ -109,7 +109,7 @@ impl FinalizeProposal<'_> { let squads_proposal_key = squads_proposal.key(); let proposal_seeds = &[ - b"proposal", + SEED_PROPOSAL, squads_proposal_key.as_ref(), &[proposal.pda_bump], ]; @@ -142,7 +142,8 @@ impl FinalizeProposal<'_> { let threshold_bps = if proposal.is_team_sponsored { dao.team_sponsored_pass_threshold_bps } else { - dao.pass_threshold_bps as i16 + // Thanks to invariants this will never error - still it's better to be safe here. + i16::try_from(dao.pass_threshold_bps).map_err(|_| FutarchyError::CastingOverflow)? }; // this can't overflow because each twap can only be MAX_PRICE (~1e31), @@ -175,7 +176,7 @@ impl FinalizeProposal<'_> { let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; let dao_signer = &[&dao_seeds[..]]; if new_proposal_state == ProposalState::Passed { diff --git a/programs/futarchy/src/instructions/initialize_dao.rs b/programs/futarchy/src/instructions/initialize_dao.rs index a2503459f..b271f51c0 100644 --- a/programs/futarchy/src/instructions/initialize_dao.rs +++ b/programs/futarchy/src/instructions/initialize_dao.rs @@ -27,7 +27,7 @@ pub struct InitializeDao<'info> { #[account( init, payer = payer, - seeds = [b"dao", dao_creator.key().as_ref(), params.nonce.to_le_bytes().as_ref()], + seeds = [SEED_DAO, dao_creator.key().as_ref(), params.nonce.to_le_bytes().as_ref()], bump, space = 8 + Dao::INIT_SPACE, )] @@ -90,7 +90,7 @@ impl InitializeDao<'_> { let creator_key = ctx.accounts.dao_creator.key(); let dao_seeds = &[ - b"dao".as_ref(), + SEED_DAO, creator_key.as_ref(), &nonce.to_le_bytes(), &[ctx.bumps.dao], diff --git a/programs/futarchy/src/instructions/initialize_proposal.rs b/programs/futarchy/src/instructions/initialize_proposal.rs index 13265e222..47d1a6b4e 100644 --- a/programs/futarchy/src/instructions/initialize_proposal.rs +++ b/programs/futarchy/src/instructions/initialize_proposal.rs @@ -7,7 +7,7 @@ pub struct InitializeProposal<'info> { init, payer = payer, space = 8 + Proposal::INIT_SPACE, - seeds = [b"proposal", squads_proposal.key().as_ref()], + seeds = [SEED_PROPOSAL, squads_proposal.key().as_ref()], bump )] pub proposal: Box>, diff --git a/programs/futarchy/src/instructions/launch_proposal.rs b/programs/futarchy/src/instructions/launch_proposal.rs index 71d32f19a..06d0defc2 100644 --- a/programs/futarchy/src/instructions/launch_proposal.rs +++ b/programs/futarchy/src/instructions/launch_proposal.rs @@ -105,6 +105,11 @@ impl LaunchProposal<'_> { let base_to_lp = spot.base_reserves / 2; let quote_to_lp = spot.quote_reserves / 2; + // Prevent launching proposals with zero reserves, which would permanently + // freeze the DAO (no swaps possible, finalize_proposal fails, no recovery path) + require_gt!(base_to_lp, 0, FutarchyError::InsufficientLiquidity); + require_gt!(quote_to_lp, 0, FutarchyError::InsufficientLiquidity); + spot.base_reserves -= base_to_lp; spot.quote_reserves -= quote_to_lp; diff --git a/programs/futarchy/src/instructions/provide_liquidity.rs b/programs/futarchy/src/instructions/provide_liquidity.rs index d009725ca..bcf0503a0 100644 --- a/programs/futarchy/src/instructions/provide_liquidity.rs +++ b/programs/futarchy/src/instructions/provide_liquidity.rs @@ -50,7 +50,7 @@ pub struct ProvideLiquidity<'info> { #[account( init_if_needed, payer = payer, - seeds = [b"amm_position", dao.key().as_ref(), params.position_authority.key().as_ref()], + seeds = [SEED_AMM_POSITION, dao.key().as_ref(), params.position_authority.key().as_ref()], bump, space = 8 + AmmPosition::INIT_SPACE, )] @@ -119,6 +119,7 @@ impl ProvideLiquidity<'_> { } else { // equivalent to $0.1 if the quote is USDC, here for rounding require_gte!(quote_amount, MIN_QUOTE_LIQUIDITY); + require_gt!(max_base_amount, 0); let base_amount = max_base_amount; diff --git a/programs/futarchy/src/instructions/spot_swap.rs b/programs/futarchy/src/instructions/spot_swap.rs index 290e48ba5..09aa2f801 100644 --- a/programs/futarchy/src/instructions/spot_swap.rs +++ b/programs/futarchy/src/instructions/spot_swap.rs @@ -100,13 +100,9 @@ impl SpotSwap<'_> { input_amount, )?; - // let dao_key = dao.key(); - // let dao_creator = dao.dao_creator; - // let nonce = dao.nonce; - // let signer_seeds = &[b"dao".as_ref(), dao_creator.as_ref(), nonce.to_le_bytes().as_ref(), &[dao.pda_bump]]; let dao_nonce = &dao.nonce.to_le_bytes(); let dao_creator_key = &dao.dao_creator.as_ref(); - let dao_seeds = &[b"dao".as_ref(), dao_creator_key, dao_nonce, &[dao.pda_bump]]; + let dao_seeds = &[SEED_DAO, dao_creator_key, dao_nonce, &[dao.pda_bump]]; token::transfer( CpiContext::new_with_signer( diff --git a/programs/futarchy/src/instructions/stake_to_proposal.rs b/programs/futarchy/src/instructions/stake_to_proposal.rs index 1c349e2dc..0a0281d26 100644 --- a/programs/futarchy/src/instructions/stake_to_proposal.rs +++ b/programs/futarchy/src/instructions/stake_to_proposal.rs @@ -28,7 +28,7 @@ pub struct StakeToProposal<'info> { #[account( init_if_needed, payer = payer, - seeds = [b"stake", proposal.key().as_ref(), staker.key().as_ref()], + seeds = [SEED_STAKE, proposal.key().as_ref(), staker.key().as_ref()], bump, space = 8 + StakeAccount::INIT_SPACE, )] diff --git a/programs/futarchy/src/instructions/unstake_from_proposal.rs b/programs/futarchy/src/instructions/unstake_from_proposal.rs index ec0af8e6f..c29328de0 100644 --- a/programs/futarchy/src/instructions/unstake_from_proposal.rs +++ b/programs/futarchy/src/instructions/unstake_from_proposal.rs @@ -27,7 +27,7 @@ pub struct UnstakeFromProposal<'info> { pub proposal_base_account: Box>, #[account( mut, - seeds = [b"stake", proposal.key().as_ref(), staker.key().as_ref()], + seeds = [SEED_STAKE, proposal.key().as_ref(), staker.key().as_ref()], bump = stake_account.bump, )] pub stake_account: Box>, @@ -70,7 +70,7 @@ impl UnstakeFromProposal<'_> { // Transfer tokens from proposal back to staker let seeds = &[ - b"proposal", + SEED_PROPOSAL, proposal.squads_proposal.as_ref(), &[proposal.pda_bump], ]; diff --git a/programs/futarchy/src/instructions/update_dao.rs b/programs/futarchy/src/instructions/update_dao.rs index 672fa26f6..7d57ebb5a 100644 --- a/programs/futarchy/src/instructions/update_dao.rs +++ b/programs/futarchy/src/instructions/update_dao.rs @@ -18,7 +18,7 @@ pub struct UpdateDaoParams { #[event_cpi] pub struct UpdateDao<'info> { #[account(mut, has_one = squads_multisig_vault)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub squads_multisig_vault: Signer<'info>, } diff --git a/programs/futarchy/src/instructions/withdraw_liquidity.rs b/programs/futarchy/src/instructions/withdraw_liquidity.rs index 4d7b773a8..9f598ca08 100644 --- a/programs/futarchy/src/instructions/withdraw_liquidity.rs +++ b/programs/futarchy/src/instructions/withdraw_liquidity.rs @@ -16,7 +16,7 @@ pub struct WithdrawLiquidityParams { #[event_cpi] pub struct WithdrawLiquidity<'info> { #[account(mut)] - pub dao: Account<'info, Dao>, + pub dao: Box>, pub position_authority: Signer<'info>, #[account( mut, @@ -44,7 +44,7 @@ pub struct WithdrawLiquidity<'info> { pub amm_quote_vault: Account<'info, TokenAccount>, #[account( mut, - seeds = [b"amm_position", dao.key().as_ref(), position_authority.key().as_ref()], + seeds = [SEED_AMM_POSITION, dao.key().as_ref(), position_authority.key().as_ref()], bump, has_one = dao, has_one = position_authority, @@ -125,7 +125,7 @@ impl WithdrawLiquidity<'_> { let dao_creator = dao.dao_creator; let nonce = dao.nonce.to_le_bytes(); let signer_seeds = &[ - b"dao".as_ref(), + SEED_DAO, dao_creator.as_ref(), nonce.as_ref(), &[dao.pda_bump], diff --git a/programs/futarchy/src/state/amm_position.rs b/programs/futarchy/src/state/amm_position.rs index 2da1fc30e..9555e9d25 100644 --- a/programs/futarchy/src/state/amm_position.rs +++ b/programs/futarchy/src/state/amm_position.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +pub const SEED_AMM_POSITION: &[u8] = b"amm_position"; + #[account] #[derive(InitSpace)] pub struct AmmPosition { diff --git a/programs/futarchy/src/state/dao.rs b/programs/futarchy/src/state/dao.rs index 24e066cae..04a5c10bb 100644 --- a/programs/futarchy/src/state/dao.rs +++ b/programs/futarchy/src/state/dao.rs @@ -1,7 +1,15 @@ pub use super::*; +pub const SEED_DAO: &[u8] = b"dao"; + pub const MAX_SPENDING_LIMIT_MEMBERS: usize = 10; +pub const MIN_PROPOSAL_DURATION_TWAP_MULTIPLIER: u32 = 2; +pub const MIN_PROPOSAL_DURATION_SECONDS: u32 = 60 * 60 * 24; +pub const MAX_PASS_THRESHOLD_BPS: u16 = 1_000; +pub const MAX_TEAM_SPONSORED_PASS_THRESHOLD_BPS: i16 = 1_000; +pub const MIN_TEAM_SPONSORED_PASS_THRESHOLD_BPS: i16 = -1_000; + #[account] #[derive(InitSpace)] pub struct Dao { @@ -69,34 +77,52 @@ impl Dao { pub fn invariant(&self) -> Result<()> { require_gte!( self.seconds_per_proposal, - self.twap_start_delay_seconds * 2, + self.twap_start_delay_seconds * MIN_PROPOSAL_DURATION_TWAP_MULTIPLIER, FutarchyError::ProposalDurationTooShort ); require_gte!( self.seconds_per_proposal, - 60 * 60 * 24, + MIN_PROPOSAL_DURATION_SECONDS, FutarchyError::ProposalDurationTooShort ); require_gte!( - 1_000, + MAX_PASS_THRESHOLD_BPS, self.pass_threshold_bps, FutarchyError::PassThresholdTooHigh ); require_gte!( self.team_sponsored_pass_threshold_bps, - -1_000, + MIN_TEAM_SPONSORED_PASS_THRESHOLD_BPS, FutarchyError::InvalidTeamSponsoredPassThreshold ); require_gte!( - 1_000, + MAX_TEAM_SPONSORED_PASS_THRESHOLD_BPS, self.team_sponsored_pass_threshold_bps, FutarchyError::InvalidTeamSponsoredPassThreshold ); + require_gt!( + self.min_base_futarchic_liquidity, + 0, + FutarchyError::InsufficientLiquidity + ); + + require_gt!( + self.min_quote_futarchic_liquidity, + 0, + FutarchyError::InsufficientLiquidity + ); + + require_gt!( + self.twap_max_observation_change_per_update, + 0u128, + FutarchyError::InvalidMaxObservationChange + ); + Ok(()) } } diff --git a/programs/futarchy/src/state/futarchy_amm.rs b/programs/futarchy/src/state/futarchy_amm.rs index d61e53911..4dc6d7cd4 100644 --- a/programs/futarchy/src/state/futarchy_amm.rs +++ b/programs/futarchy/src/state/futarchy_amm.rs @@ -268,16 +268,16 @@ impl PoolState { #[derive(Default, Clone, Copy, Debug, AnchorDeserialize, AnchorSerialize, InitSpace)] pub struct TwapOracle { - /// Running sum of slots_per_last_update * last_observation. + /// Running sum of seconds_since_last_update * last_observation. /// /// Assuming latest observations are as big as possible (u64::MAX * 1e12), - /// we can store 18 million slots worth of observations, which turns out to - /// be ~85 days worth of slots. + /// we can store 18 million seconds worth of observations, which turns out to + /// be ~208 days. /// /// Assuming that latest observations are 100x smaller than they could theoretically - /// be, we can store 8500 days (23 years) worth of them. Even this is a very + /// be, we can store ~57 years worth of them. Even this is a very /// very conservative assumption - META/USDC prices should be between 1e9 and - /// 1e15, which would overflow after 1e15 years worth of slots. + /// 1e15, which would overflow after 1e15 years. /// /// So in the case of an overflow, the aggregator rolls back to 0. It's the /// client's responsibility to sanity check the assets or to handle an @@ -400,13 +400,13 @@ impl Pool { let effective_last_updated_timestamp = oracle.last_updated_timestamp.max(twap_start_timestamp); - let slot_difference: u128 = (current_timestamp - effective_last_updated_timestamp) + let time_difference: u128 = (current_timestamp - effective_last_updated_timestamp) .try_into() .unwrap(); // if this saturates, the aggregator will wrap back to 0, so this value doesn't // really matter. we just can't panic. - let weighted_observation = new_observation.saturating_mul(slot_difference); + let weighted_observation = new_observation.saturating_mul(time_difference); oracle.aggregator.wrapping_add(weighted_observation) }; @@ -668,6 +668,15 @@ pub fn arbitrage_after_spot_swap( } } + // No profitable arbitrage found — skip the feeless swaps to save compute + if best_input_amount == 0 { + return Ok(ArbitrageResult { + spot_profit: 0, + pass_profit: 0, + fail_profit: 0, + }); + } + let final_spot_output = spot .feeless_swap(best_input_amount, spot_direction) .unwrap(); @@ -789,6 +798,15 @@ pub fn arbitrage_after_conditional_swap( } } + // No profitable arbitrage found — skip the feeless swaps to save compute + if best_arb_input_amount == 0 { + return Ok(ArbitrageResult { + spot_profit: 0, + pass_profit: 0, + fail_profit: 0, + }); + } + let final_pass_output = pass .feeless_swap(best_arb_input_amount, conditional_direction) .unwrap(); diff --git a/programs/futarchy/src/state/proposal.rs b/programs/futarchy/src/state/proposal.rs index ae8ff9e71..331bf9e21 100644 --- a/programs/futarchy/src/state/proposal.rs +++ b/programs/futarchy/src/state/proposal.rs @@ -1,5 +1,7 @@ use super::*; +pub const SEED_PROPOSAL: &[u8] = b"proposal"; + #[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, InitSpace)] pub enum ProposalState { Draft { amount_staked: u64 }, diff --git a/programs/futarchy/src/state/stake_account.rs b/programs/futarchy/src/state/stake_account.rs index cd3f35815..afd44f122 100644 --- a/programs/futarchy/src/state/stake_account.rs +++ b/programs/futarchy/src/state/stake_account.rs @@ -1,5 +1,7 @@ use super::*; +pub const SEED_STAKE: &[u8] = b"stake"; + #[account] #[derive(InitSpace)] pub struct StakeAccount { diff --git a/programs/v06_launchpad/src/instructions/complete_launch.rs b/programs/v06_launchpad/src/instructions/complete_launch.rs index 62e746942..9704633f1 100644 --- a/programs/v06_launchpad/src/instructions/complete_launch.rs +++ b/programs/v06_launchpad/src/instructions/complete_launch.rs @@ -400,8 +400,8 @@ impl CompleteLaunch<'_> { twap_initial_observation: launch_price_1e12, twap_max_observation_change_per_update: launch_price_1e12 / 20, // We're providing liquidity, so that can be used for proposals - min_quote_futarchic_liquidity: 0, - min_base_futarchic_liquidity: 0, + min_quote_futarchic_liquidity: 1, + min_base_futarchic_liquidity: 1, pass_threshold_bps: 150, base_to_stake: TOKENS_TO_PARTICIPANTS / 20, seconds_per_proposal: 3 * 24 * 60 * 60, diff --git a/programs/v07_launchpad/src/instructions/complete_launch.rs b/programs/v07_launchpad/src/instructions/complete_launch.rs index cf3c98af6..8a7177386 100644 --- a/programs/v07_launchpad/src/instructions/complete_launch.rs +++ b/programs/v07_launchpad/src/instructions/complete_launch.rs @@ -423,8 +423,8 @@ impl CompleteLaunch<'_> { twap_initial_observation: launch_price_1e12, twap_max_observation_change_per_update: launch_price_1e12 / 20, // We're providing liquidity, so that can be used for proposals - min_quote_futarchic_liquidity: 0, - min_base_futarchic_liquidity: 0, + min_quote_futarchic_liquidity: 1, + min_base_futarchic_liquidity: 1, pass_threshold_bps: 300, base_to_stake: PROPOSAL_MIN_STAKE_TOKENS, seconds_per_proposal: 3 * 24 * 60 * 60, diff --git a/sdk/src/v0.7/types/futarchy.ts b/sdk/src/v0.7/types/futarchy.ts index 92b38e277..fc8d3ddd4 100644 --- a/sdk/src/v0.7/types/futarchy.ts +++ b/sdk/src/v0.7/types/futarchy.ts @@ -1992,16 +1992,16 @@ export type Futarchy = { { name: "aggregator"; docs: [ - "Running sum of slots_per_last_update * last_observation.", + "Running sum of seconds_since_last_update * last_observation.", "", "Assuming latest observations are as big as possible (u64::MAX * 1e12),", - "we can store 18 million slots worth of observations, which turns out to", - "be ~85 days worth of slots.", + "we can store 18 million seconds worth of observations, which turns out to", + "be ~208 days.", "", "Assuming that latest observations are 100x smaller than they could theoretically", - "be, we can store 8500 days (23 years) worth of them. Even this is a very", + "be, we can store ~57 years worth of them. Even this is a very", "very conservative assumption - META/USDC prices should be between 1e9 and", - "1e15, which would overflow after 1e15 years worth of slots.", + "1e15, which would overflow after 1e15 years.", "", "So in the case of an overflow, the aggregator rolls back to 0. It's the", "client's responsibility to sanity check the assets or to handle an", @@ -5250,16 +5250,16 @@ export const IDL: Futarchy = { { name: "aggregator", docs: [ - "Running sum of slots_per_last_update * last_observation.", + "Running sum of seconds_since_last_update * last_observation.", "", "Assuming latest observations are as big as possible (u64::MAX * 1e12),", - "we can store 18 million slots worth of observations, which turns out to", - "be ~85 days worth of slots.", + "we can store 18 million seconds worth of observations, which turns out to", + "be ~208 days.", "", "Assuming that latest observations are 100x smaller than they could theoretically", - "be, we can store 8500 days (23 years) worth of them. Even this is a very", + "be, we can store ~57 years worth of them. Even this is a very", "very conservative assumption - META/USDC prices should be between 1e9 and", - "1e15, which would overflow after 1e15 years worth of slots.", + "1e15, which would overflow after 1e15 years.", "", "So in the case of an overflow, the aggregator rolls back to 0. It's the", "client's responsibility to sanity check the assets or to handle an",