usdc-plus-exchange 0.1.8

USDC <-> USDC+ exchange library for the Reflect protocol.
Documentation
use anchor_lang::prelude::{*, borsh::BorshSchema};
use std::io::Write;
use crate::drift::SafeMath;
use crate::errors::ReflectErrorCodes;
use crate::reflect::helpers::{calc_all_cuts, compute_receipt_token};

#[repr(C)]
#[derive(BorshSchema, AnchorDeserialize, AnchorSerialize, Default, Debug, Clone, InitSpace, PartialEq, Eq)]
pub struct AutoCompound {           
   /// Total collateral **amount** currently attributed to vault holders.
   /// - Units: **collateral (base units)**
   /// - Includes: all user deposits + all pool allocations (index 0 cut) captured so far
   /// - Excludes: any value queued for extraction to fee recipients (Q)
   /// - Mutates on:
   ///     * deposit  → V += deposit_collateral
   ///     * redeem   → V -= redeemed_collateral
   ///     * capture  → V += pool_allocation_collateral (index 0 share only)
   pub deposited_vault_value: u64,

   /// Net user cashflow since the last capture.
   /// - Units: **collateral (signed)**
   /// - Purpose: isolate organic PnL from user flows when computing capturable yield
   /// - Updates:
   ///     * after deposit  → += deposit_collateral
   ///     * after redeem   → -= redeemed_collateral
   ///     * after capture  → reset to 0
   pub net_user_flow_since_capture: i64,

   /// Vault's observed total **collateral** at the most recent capture boundary.
   /// - Units: **collateral**
   /// - Used as the baseline for `refresh_autocompounder(current_total_collateral)`
   /// - On capture:
   ///     * compute yield/loss vs `last_pool_value + net_user_flow_since_capture`
   ///     * then set `last_pool_value = post_capture_pool_value`
   pub last_pool_value: u64,

   /// Total receipt tokens currently queued to be minted to recipients.
   /// - Units: **receipt tokens**
   /// - Semantics: recipients' portion of profit, converted to receipt tokens at the current PPS
   /// - On capture/process_yield:
   ///     * Q_receipt += receipt_rate(recipients_collateral)
   /// - On distribution (mint step):
   ///     * consumer reads Q_receipt, mints, then resets/decrements Q_receipt
   pub queued_recipient_shares: u64,
}

impl AutoCompound {

    pub fn update_total_capturable_yield(&mut self, value: u64){
        self.queued_recipient_shares = value;      
    }

    pub fn calculate_capturable_yield(&self, current_value: u64) -> Result<i64> {
        // Expected value = last recorded value + net user flows.
        let expected_value = if self.net_user_flow_since_capture >= 0 {
            self.last_pool_value.safe_add(self.net_user_flow_since_capture as u64)?
        } else {
            self.last_pool_value.safe_sub((-self.net_user_flow_since_capture) as u64)?
        };
        
        // Calculate yield as signed value (can be negative for losses).
        let yield_or_loss = (current_value as i128) - (expected_value as i128);        
        
        if yield_or_loss > i64::MAX as i128 {
            return Err(ReflectErrorCodes::MathError.into());
        } else if yield_or_loss < i64::MIN as i128 {
            return Err(ReflectErrorCodes::MathError.into());
        }
        
        Ok(yield_or_loss as i64)
    }

    pub fn update_pool_value(&mut self, value: u64){
        self.last_pool_value = value;      
    }
    
    /// Settle yield/loss vs. the baseline, book pool keep into V (USDC),
    /// queue **recipient mints** as SHARES (USDC+), and advance the baseline.
    ///
    /// ### Invariants & Units
    /// - `current_total_usdc`: observed assets under management (USDC).
    /// - `cuts_bp[0]`: pool keep (stays in V, **no mint** to index 0).
    /// - `cuts_bp[1..]`: recipients to **mint** (SHARES) later.
    /// - `deposited_vault_value (V)`: USDC.
    /// - `effective_supply (S)`: SHARES (USDC+ tokens).
    /// - `queued_recipient_shares (Q)`: **SHARES to mint** to recipients i≥1 (not value).
    ///
    /// ### Cases
    /// 1) Loss: V -= loss; Q unchanged; baseline = current_total_usdc.
    /// 2) No change: nothing; baseline = current_total_usdc.
    /// 3) Profit:
    ///    - Split profit in **USDC** with `cuts_bp`.
    ///    - Add pool keep (index 0) to **V** (PPS ↑), **mint nothing** for index 0.
    ///    - Convert recipients' **USDC** portion to **SHARES** at **current PPS** (after pool keep is booked).
    ///    - Q += shares_for_recipients.
    ///    - Baseline = current_total_usdc (no assets left; we’re minting later).
    ///
    /// ### Why baseline = current_total_usdc on profit?
    /// Minting shares to recipients does not move USDC out of the pool; it inflates S.
    /// Therefore the on-chain assets remain the observed amount; only PPS is affected when
    /// we later mint those shares.
    pub fn update_pool(&mut self, current_total_usdc: u64, cuts_bp: &[u16], token_supply: u64) -> Result<()> {
        // Compute organic signed PnL vs the moving baseline:        
        // y = current_total_usdc - (last_pool_value +/- net_user_flow_since_capture)        
        let y: i64 = self.calculate_capturable_yield(current_total_usdc)?;
        
        // --------- LOSS PATH ---------
        if y < 0 {            
            // Book the loss directly into V. No recipient queue changes.            
            self.deposited_vault_value = self.deposited_vault_value.safe_sub(y.unsigned_abs())?;

        // --------- PROFIT PATH ---------
        } else if y > 0 {            
            // Split profit as: pool_keep (index 0) + recipients (i>=1).
            let profit_usdc: u64 = y as u64;
            let amounts_usdc: Vec<u64> = calc_all_cuts(profit_usdc, cuts_bp.to_vec())?;
            let pool_keep_usdc = amounts_usdc.get(0).copied().unwrap_or(0);
            let recipients_usdc = profit_usdc.safe_sub(pool_keep_usdc)?;

            // Let profit_usdc = pool_keep_usdc + recipients_usdc.
            // No USDC leaves the external vault during capture; recipients (i>=1) are paid via new share issuance.
            // Therefore the vault's assets under management (V) must increase by the FULL profit:
            //     V_after = V_before + profit_usdc
            //             = V_before + (pool_keep_usdc + recipients_usdc)
            // If we only added pool_keep_usdc to V and then minted recipient shares (S↑),
            // PPS = V/S would be depressed, enabling cheap mints and underpaying redemptions until the next capture.
            self.deposited_vault_value = self.deposited_vault_value.safe_add(profit_usdc)?;

            // Now compute how many shares to mint to recipients so their post-mint value equals exactly recipients_usdc with no dilution:
            if recipients_usdc > 0 {                           
                require!(token_supply > 0, ReflectErrorCodes::MathError);
                require!(self.deposited_vault_value > recipients_usdc, ReflectErrorCodes::MathError);

                // Denominator should be V_before + pool_keep_usdc:
                let v_minus_r: u64 = self.deposited_vault_value.safe_sub(recipients_usdc)?;

                // Solve:  shares to mint = recipients_usdc * S / (V_after - recipients_usdc)                
                let shares_to_mint: u64 = compute_receipt_token(recipients_usdc, v_minus_r, token_supply)?;

                // Shares minted to recipients by the caller in the same tx path.
                self.queued_recipient_shares = self.queued_recipient_shares.safe_add(shares_to_mint)?;
            }
            // Note: no shares to index 0 in AutoCompound mode; pool_keep_usdc is already
            // reflected in V and benefits existing LPs via higher PPS.
        } 

        // Advance the capture baseline to the observed total vault collateral.
        self.last_pool_value = current_total_usdc;

        // Reset net user flows for the next interval.
        self.net_user_flow_since_capture = 0;

        Ok(())
    }
    
    pub fn deserialize(buf: &mut &[u8]) -> Result<Self> {        
        let deposited_vault_value = u64::deserialize(buf)?;
        let net_user_flow_since_capture = i64::deserialize(buf)?;
        let last_pool_value = u64::deserialize(buf)?;
        let queued_recipient_shares = u64::deserialize(buf)?;
        
        Ok(AutoCompound {            
            deposited_vault_value,
            net_user_flow_since_capture,
            last_pool_value,
            queued_recipient_shares,
        })
    }
    
    pub fn try_serialise<W: Write>(&self, writer: &mut W) -> Result<()> {
        self.deposited_vault_value.serialize(writer)?;
        self.net_user_flow_since_capture.serialize(writer)?;
        self.last_pool_value.serialize(writer)?;
        self.queued_recipient_shares.serialize(writer)?;
        Ok(())
    }
}


pub const AUTOCOMPOUND_START: usize = 1026;
pub fn deserialise_autocompound(data_usdc_controller: &[u8]) -> Result<AutoCompound> {
    if data_usdc_controller.len() < AUTOCOMPOUND_START + 40 {
        return Err(ReflectErrorCodes::InsufficientData.into());
    }
    
    let mut slice = &data_usdc_controller[AUTOCOMPOUND_START..];
    AutoCompound::deserialize(&mut slice)
        .map_err(|_| ReflectErrorCodes::DeserializationError.into())
}