Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/v16_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10103,6 +10103,14 @@ pub mod processor {
//
// available = principal - (loss - recovery)
ledger.total_principal_atoms = gross_principal_atoms;

// Legacy buckets may already have outstanding provider earnings when
// the canonical ledger is created. Seed that outstanding balance into
// the migration baseline before pinning the observation watermark;
// otherwise pre-ledger earnings would never be recognized by sync.
ledger.total_earnings_atoms = bucket.utilization_fee_earnings;
ledger.last_observed_bucket_earnings_atoms = bucket.utilization_fee_earnings;

ledger.cumulative_loss_atoms = unavailable_atoms;
ledger.cumulative_recovery_atoms = 0;

Expand Down Expand Up @@ -10534,13 +10542,35 @@ pub mod processor {
let (cfg, group) = state::market_view_mut(&mut market_data)?;
let configured_slots = group.header.config.max_market_slots.get() as usize;
let asset_index = domain_usize / 2;
if group.header.mode != 0
|| domain_usize >= configured_slots.saturating_mul(2)
if domain_usize >= configured_slots.saturating_mul(2)
|| asset_index >= configured_slots
{
return Err(PercolatorError::EngineLockActive.into());
}
require_domain_accepts_live_topup_view(&group, domain_usize)?;

match group.header.mode {
// Normal backing deposits remain strictly Live-only.
0 => {
require_domain_accepts_live_topup_view(&group, domain_usize)?;
}

// #433 migration-only exception: a fully wound-down Resolved
// market may still contain backing funded before canonical
// BackingDomainLedger creation became mandatory.
//
// amount == 0 performs accounting reconciliation only. It must
// not reopen backing deposits or mutate engine backing.
1 if amount == 0 => {
if group.header.materialized_portfolio_count.get() != 0
|| group.header.c_tot.get() != 0
{
return Err(PercolatorError::EngineLockActive.into());
}
}

// Non-zero top-ups remain forbidden after resolution.
_ => return Err(PercolatorError::EngineLockActive.into()),
}
let profile = read_oracle_profile_from_view(&group, &cfg, asset_index)?;
let authorities = domain_authorities_from_profile(&cfg, &profile, asset_index);
(cfg, authorities)
Expand Down
308 changes: 308 additions & 0 deletions tests/v16_cu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12186,3 +12186,311 @@ fn v16_bpf_legacy_ledgerless_nonzero_topup_does_not_book_refill_as_recovery() {
);
assert_eq!(synced.last_observed_unavailable_principal_atoms, 20);
}

#[test]
fn v16_bpf_legacy_ledgerless_migration_seeds_outstanding_backing_earnings() {
const DOMAIN: u16 = 1;

let mut env = V16CuEnv::new();
let ledger =
state::derive_lp_backing_ledger(&env.program_id, &env.market, DOMAIN).0;

// Recreate a legacy funded domain whose canonical ledger never existed.
seed_legacy_ledgerless_consumed_backing(
&mut env,
DOMAIN as usize,
60,
40,
);

// Historical provider earnings also existed before the ledger did.
env.mutate_market(|_cfg, group| {
group.source_backing_buckets[DOMAIN as usize].utilization_fee_earnings = 30;
group.vault = group
.vault
.checked_add(30)
.expect("#433 legacy earnings vault overflow");
});

env.set_token_account_amount(
env.vault,
env.mint,
env.vault_authority,
130,
);

assert!(
env.svm.get_account(&ledger).is_none(),
"legacy earnings fixture must start without a canonical ledger"
);

let admin = env.admin.insecure_clone();
let payer = env.payer.insecure_clone();
let pid = env.program_id;
let market = env.market;
let vault = env.vault;
let vault_authority = env.vault_authority;
let zero_source = env.token_account(admin.pubkey(), 0);

// Migration must snapshot BOTH principal/loss and already-outstanding
// provider earnings.
send_tx(
&mut env.svm,
pid,
&payer,
ProgInstruction::TopUpBackingBucket {
domain: DOMAIN,
amount: 0,
expiry_slot: 10,
},
vec![
AccountMeta::new(admin.pubkey(), true),
AccountMeta::new(market, false),
AccountMeta::new(zero_source, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
AccountMeta::new_readonly(solana_sdk::system_program::ID, false),
],
&[&admin],
)
.expect("#433 migration must reconcile outstanding legacy backing earnings");

let ledger_account = env
.svm
.get_account(&ledger)
.expect("migration must create the canonical ledger");
let migrated =
state::read_backing_domain_ledger(&ledger_account.data).unwrap();

assert_eq!(migrated.total_principal_atoms, 100);
assert_eq!(migrated.cumulative_loss_atoms, 40);

assert_eq!(
migrated.total_earnings_atoms, 30,
"outstanding pre-ledger earnings must become the migration baseline"
);
assert_eq!(
migrated.total_earnings_withdrawn_atoms, 0,
"migration must not invent historical earnings withdrawals"
);
assert_eq!(
migrated.last_observed_bucket_earnings_atoms, 30,
"earnings watermark must match the same migration snapshot"
);

// Prove those migrated earnings can subsequently be withdrawn without
// making withdrawn earnings exceed recognized earnings.
env.svm.expire_blockhash();
let dest = env.token_account(admin.pubkey(), 0);

send_tx(
&mut env.svm,
pid,
&payer,
ProgInstruction::WithdrawBackingBucketEarnings {
domain: DOMAIN,
amount: 20,
},
vec![
AccountMeta::new(admin.pubkey(), true),
AccountMeta::new(market, false),
AccountMeta::new(ledger, false),
AccountMeta::new(dest, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
],
&[&admin],
)
.expect("migrated historical backing earnings must remain withdrawable");

assert_eq!(env.token_amount(dest), 20);
assert_eq!(env.token_amount(vault), 110);

let ledger_after = env.svm.get_account(&ledger).unwrap();
let ledger_after =
state::read_backing_domain_ledger(&ledger_after.data).unwrap();

assert_eq!(ledger_after.total_earnings_atoms, 30);
assert_eq!(ledger_after.total_earnings_withdrawn_atoms, 20);
assert_eq!(ledger_after.last_observed_bucket_earnings_atoms, 10);

let (_, group_after) = env.market_state();
assert_eq!(
group_after.source_backing_buckets[DOMAIN as usize].utilization_fee_earnings,
10
);
}

#[test]
fn v16_bpf_legacy_ledgerless_resolved_zero_topup_reconciles_without_reopening_deposits() {
const DOMAIN: u16 = 1;

let mut env = V16CuEnv::new();
let ledger =
state::derive_lp_backing_ledger(&env.program_id, &env.market, DOMAIN).0;

// Legacy backing exists, but the canonical ledger does not.
seed_legacy_ledgerless_consumed_backing(
&mut env,
DOMAIN as usize,
100,
0,
);

assert!(
env.svm.get_account(&ledger).is_none(),
"resolved migration fixture must begin ledgerless"
);

// Enter the normal terminal wind-down state.
env.resolve();

let (_, resolved) = env.market_state();

assert_eq!(
resolved.mode,
percolator::MarketModeV16::Resolved,
"fixture must actually be resolved"
);
assert_eq!(
resolved.materialized_portfolio_count, 0,
"resolved migration requires full user wind-down"
);
assert_eq!(
resolved.c_tot, 0,
"resolved migration requires zero remaining user capital"
);

let admin = env.admin.insecure_clone();
let payer = env.payer.insecure_clone();
let pid = env.program_id;
let market = env.market;
let vault = env.vault;
let vault_authority = env.vault_authority;

// Negative control: allowing reconciliation must NOT reopen real deposits.
let source = env.token_account(admin.pubkey(), 1);
let vault_before = env.token_amount(vault);

let nonzero = send_tx(
&mut env.svm,
pid,
&payer,
ProgInstruction::TopUpBackingBucket {
domain: DOMAIN,
amount: 1,
expiry_slot: 20,
},
vec![
AccountMeta::new(admin.pubkey(), true),
AccountMeta::new(market, false),
AccountMeta::new(source, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
AccountMeta::new_readonly(solana_sdk::system_program::ID, false),
],
&[&admin],
);

assert!(
nonzero.is_err(),
"resolved migration exception must never reopen non-zero backing deposits"
);
assert_eq!(env.token_amount(source), 1);
assert_eq!(env.token_amount(vault), vault_before);
assert!(
env.svm.get_account(&ledger).is_none(),
"failed non-zero resolved top-up must not leave the ledger PDA behind"
);

// Positive case: amount == 0 is accounting migration only.
env.svm.expire_blockhash();

send_tx(
&mut env.svm,
pid,
&payer,
ProgInstruction::TopUpBackingBucket {
domain: DOMAIN,
amount: 0,
expiry_slot: 20,
},
vec![
AccountMeta::new(admin.pubkey(), true),
AccountMeta::new(market, false),
AccountMeta::new(source, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
AccountMeta::new_readonly(solana_sdk::system_program::ID, false),
],
&[&admin],
)
.expect(
"#433 resolved zero-capital migration must create the missing canonical ledger",
);

assert_eq!(
env.token_amount(source), 1,
"zero-capital migration must not consume source tokens"
);
assert_eq!(
env.token_amount(vault), vault_before,
"zero-capital migration must not change vault balance"
);

let ledger_account = env
.svm
.get_account(&ledger)
.expect("resolved migration must create the canonical ledger");

let migrated =
state::read_backing_domain_ledger(&ledger_account.data).unwrap();

assert_eq!(migrated.total_principal_atoms, 100);
assert_eq!(migrated.cumulative_loss_atoms, 0);
assert_eq!(migrated.cumulative_recovery_atoms, 0);
assert_eq!(
migrated.total_deposited_atoms, 0,
"migration must not fabricate historical deposit flow"
);

// Existing resolved backing withdrawal must work after reconciliation.
env.svm.expire_blockhash();

let dest = env.token_account(admin.pubkey(), 0);

send_tx(
&mut env.svm,
pid,
&payer,
ProgInstruction::WithdrawBackingBucket {
domain: DOMAIN,
amount: 40,
},
vec![
AccountMeta::new(admin.pubkey(), true),
AccountMeta::new(market, false),
AccountMeta::new(dest, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&admin],
)
.expect("reconciled resolved legacy backing must be withdrawable");

assert_eq!(env.token_amount(dest), 40);
assert_eq!(env.token_amount(vault), vault_before - 40);

let ledger_after = env.svm.get_account(&ledger).unwrap();
let ledger_after =
state::read_backing_domain_ledger(&ledger_after.data).unwrap();

assert_eq!(ledger_after.total_principal_atoms, 60);
assert_eq!(ledger_after.total_principal_withdrawn_atoms, 40);
}
Loading