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
70 changes: 29 additions & 41 deletions src/v16_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10403,16 +10403,19 @@ pub mod processor {
let vault_token = account(accounts, 3)?;
let vault_authority_ai = account(accounts, 4)?;
let token_program = account(accounts, 5)?;
let ledger_ai = accounts.get(6);
// Principal withdrawals must always update the backing-domain ledger.
// Treating this trailing account as optional allowed callers to skip
// both the principal-consistency check and the ledger decrement.
let ledger_ai = account(accounts, 6)?;
expect_signer(authority)?;
expect_writable(market_ai)?;
expect_writable(dest_token)?;
expect_writable(vault_token)?;
expect_owner(market_ai, program_id)?;
if let Some(ledger_ai) = ledger_ai {
expect_writable(ledger_ai)?;
expect_owner(ledger_ai, program_id)?;
}
expect_writable(ledger_ai)?;
expect_owner(ledger_ai, program_id)?;
let (ledger_pda, _) = state::derive_lp_backing_ledger(program_id, market_ai.key, domain);
expect_key(ledger_ai, &ledger_pda)?;
Comment on lines +10415 to +10418

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create the canonical ledger before requiring it for withdrawals

For any ordinary backing domain that has not used the LP-vault deposit path, this makes every principal withdrawal impossible: handle_top_up_backing_bucket still accepts deposits without a ledger at src/v16_program.rs:10301-10318, and the only repository path that creates this PDA is the LP-vault-specific deposit flow at src/v16_program.rs:14726-14762. A caller cannot independently create a program-owned PDA, while a zeroed ledger would record zero principal and fail the new ceiling check. Thus both historical and newly accepted ledgerless top-ups become stranded; the program needs a generic canonical-ledger initialization/migration path, or must create and require the ledger when accepting every top-up.

Useful? React with 👍 / 👎.

verify_token_program(token_program)?;
if amount == 0 {
return Err(PercolatorError::InvalidInstruction.into());
Expand Down Expand Up @@ -10476,46 +10479,31 @@ pub mod processor {
};

let (_, bucket) = backing_domain_parts_view(&group, domain_usize)?;
let mut ledger_data = if let Some(ledger_ai) = ledger_ai {
Some(ledger_ai.try_borrow_mut_data()?)
} else {
None
};
let mut ledger_state = if let Some(data) = ledger_data.as_deref() {
let (mut ledger, initialized) = read_or_new_backing_domain_ledger(
data,
market_ai.key.to_bytes(),
ledger_authority,
domain,
&bucket,
)?;
sync_backing_domain_ledger(&mut ledger, &bucket)?;
if amount > ledger.total_principal_atoms {
return Err(PercolatorError::EngineCounterUnderflow.into());
}
Some((ledger, initialized))
} else {
None
};
let mut ledger_data = ledger_ai.try_borrow_mut_data()?;
let (mut ledger, initialized) = read_or_new_backing_domain_ledger(
&ledger_data,
market_ai.key.to_bytes(),
ledger_authority,
domain,
Comment on lines +10483 to +10487

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migrate ledger ownership when rotating backing authority

After an initialized domain's backing authority is legitimately rotated through UpdateAssetAuthority, this passes the new authority into read_or_new_backing_domain_ledger, but that function rejects whenever the ledger's stored authority is the previous key (src/v16_program.rs:10103-10109). No rotation path updates the canonical ledger, so the new authorized holder cannot withdraw or perform a ledger-backed top-up; if the old key was retired, the principal is permanently stranded. The authority rotation must migrate the canonical ledger atomically or ledger validation must accommodate the authorized rotation.

Useful? React with 👍 / 👎.

&bucket,
)?;
sync_backing_domain_ledger(&mut ledger, &bucket)?;
if amount > ledger.total_principal_atoms {
return Err(PercolatorError::EngineCounterUnderflow.into());
}
group
.withdraw_fresh_counterparty_backing_not_atomic(domain_usize, amount)
.map_err(map_v16_error)?;
if let Some((ledger, _)) = ledger_state.as_mut() {
ledger.total_principal_atoms = ledger
.total_principal_atoms
.checked_sub(amount)
.ok_or(PercolatorError::EngineCounterUnderflow)?;
ledger.total_principal_withdrawn_atoms = ledger
.total_principal_withdrawn_atoms
.checked_add(amount)
.ok_or(PercolatorError::EngineArithmeticOverflow)?;
}
ledger.total_principal_atoms = ledger
.total_principal_atoms
.checked_sub(amount)
.ok_or(PercolatorError::EngineCounterUnderflow)?;
ledger.total_principal_withdrawn_atoms = ledger
.total_principal_withdrawn_atoms
.checked_add(amount)
.ok_or(PercolatorError::EngineArithmeticOverflow)?;
group.validate_shape().map_err(map_v16_error)?;
if let (Some(data), Some((ledger, initialized))) =
(ledger_data.as_deref_mut(), ledger_state.as_ref())
{
write_or_init_backing_domain_ledger(data, ledger, *initialized)?;
}
write_or_init_backing_domain_ledger(&mut ledger_data, &ledger, initialized)?;
}

let bump_arr = [bump];
Expand Down
111 changes: 111 additions & 0 deletions tests/v16_cu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,25 @@ impl V16CuEnv {
self.program_account(state::backing_domain_ledger_account_len())
}

fn canonical_backing_domain_ledger_account(&mut self, domain: u16) -> Pubkey {
let (ledger, _) = state::derive_lp_backing_ledger(&self.program_id, &self.market, domain);
if self.svm.get_account(&ledger).is_none() {
self.svm
.set_account(
ledger,
Account {
lamports: 1_000_000_000,
data: vec![0u8; state::backing_domain_ledger_account_len()],
owner: self.program_id,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
}
ledger
}

fn insurance_ledger_account(&mut self) -> Pubkey {
self.program_account(state::insurance_ledger_account_len())
}
Expand Down Expand Up @@ -2171,6 +2190,7 @@ impl V16CuEnv {
amount: u128,
expiry_slot: u64,
) -> u64 {
let ledger = self.canonical_backing_domain_ledger_account(domain);
send_tx(
&mut self.svm,
self.program_id,
Expand All @@ -2186,6 +2206,7 @@ impl V16CuEnv {
AccountMeta::new(source, false),
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&self.admin],
)
Expand Down Expand Up @@ -2305,6 +2326,7 @@ impl V16CuEnv {
amount: u128,
expiry_slot: u64,
) -> (Pubkey, u64) {
let ledger = self.canonical_backing_domain_ledger_account(domain);
let source = Pubkey::new_unique();
self.svm
.set_account(
Expand Down Expand Up @@ -2333,6 +2355,7 @@ impl V16CuEnv {
AccountMeta::new(source, false),
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&self.admin],
)
Expand Down Expand Up @@ -2390,6 +2413,7 @@ impl V16CuEnv {
amount: u128,
expiry_slot: u64,
) -> Pubkey {
let ledger = self.canonical_backing_domain_ledger_account(domain);
self.ensure_signer_account(authority.pubkey());
let source = self.token_account(authority.pubkey(), amount as u64);
send_tx(
Expand All @@ -2407,6 +2431,7 @@ impl V16CuEnv {
AccountMeta::new(source, false),
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[authority],
)
Expand Down Expand Up @@ -2549,6 +2574,7 @@ impl V16CuEnv {
domain: u16,
amount: u128,
) -> u64 {
let ledger = self.canonical_backing_domain_ledger_account(domain);
send_tx(
&mut self.svm,
self.program_id,
Expand All @@ -2561,6 +2587,7 @@ impl V16CuEnv {
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(self.vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&self.admin],
)
Expand All @@ -2573,6 +2600,7 @@ impl V16CuEnv {
domain: u16,
amount: u128,
) -> Result<u64, String> {
let ledger = self.canonical_backing_domain_ledger_account(domain);
send_tx(
&mut self.svm,
self.program_id,
Expand All @@ -2585,6 +2613,7 @@ impl V16CuEnv {
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(self.vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&self.admin],
)
Expand All @@ -2605,6 +2634,7 @@ impl V16CuEnv {
domain: u16,
amount: u128,
) -> u64 {
let ledger = self.canonical_backing_domain_ledger_account(domain);
self.ensure_signer_account(authority.pubkey());
send_tx(
&mut self.svm,
Expand All @@ -2618,6 +2648,7 @@ impl V16CuEnv {
AccountMeta::new(self.vault, false),
AccountMeta::new_readonly(self.vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[authority],
)
Expand Down Expand Up @@ -3717,6 +3748,84 @@ fn v16_bpf_privileged_reactivate_uses_authenticated_slot() {
);
}

#[test]
fn v16_bpf_withdraw_backing_bucket_requires_canonical_ledger() {
let mut env = V16CuEnv::new();
let domain = 1;
let ledger = env.canonical_backing_domain_ledger_account(domain);
env.top_up_backing_bucket(domain, 100, 10);
let dest = env.token_account(env.admin.pubkey(), 0);

let market_before = env.svm.get_account(&env.market).unwrap().data;
let ledger_before = env.svm.get_account(&ledger).unwrap().data;
let vault_before = env.token_amount(env.vault);
let dest_before = env.token_amount(dest);
let impostor = env.backing_domain_ledger_account();

let withdraw_accounts = |ledger: Option<Pubkey>| {
let mut accounts = vec![
AccountMeta::new(env.admin.pubkey(), true),
AccountMeta::new(env.market, false),
AccountMeta::new(dest, false),
AccountMeta::new(env.vault, false),
AccountMeta::new_readonly(env.vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
];
if let Some(ledger) = ledger {
accounts.push(AccountMeta::new(ledger, false));
}
accounts
};

let omitted = send_tx(
&mut env.svm,
env.program_id,
&env.payer,
ProgInstruction::WithdrawBackingBucket { domain, amount: 40 },
withdraw_accounts(None),
&[&env.admin],
);
assert!(omitted.is_err(), "omitting the ledger must fail closed");

env.svm.expire_blockhash();
let substituted = send_tx(
&mut env.svm,
env.program_id,
&env.payer,
ProgInstruction::WithdrawBackingBucket { domain, amount: 40 },
withdraw_accounts(Some(impostor)),
&[&env.admin],
);
assert!(
substituted.is_err(),
"a noncanonical program-owned ledger must fail closed"
);
assert_eq!(env.svm.get_account(&env.market).unwrap().data, market_before);
assert_eq!(env.svm.get_account(&ledger).unwrap().data, ledger_before);
assert_eq!(env.token_amount(env.vault), vault_before);
assert_eq!(env.token_amount(dest), dest_before);

env.svm.expire_blockhash();
send_tx(
&mut env.svm,
env.program_id,
&env.payer,
ProgInstruction::WithdrawBackingBucket { domain, amount: 40 },
withdraw_accounts(Some(ledger)),
&[&env.admin],
)
.expect("canonical ledger withdrawal");

let ledger_after = state::read_backing_domain_ledger(
&env.svm.get_account(&ledger).unwrap().data,
)
.unwrap();
assert_eq!(ledger_after.total_principal_atoms, 60);
assert_eq!(ledger_after.total_principal_withdrawn_atoms, 40);
assert_eq!(env.token_amount(env.vault), 60);
assert_eq!(env.token_amount(dest), 40);
}

#[test]
fn v16_bpf_permissionless_oracle_liquidation_uses_only_its_own_domain_insurance() {
let mut env = V16CuEnv::new();
Expand Down Expand Up @@ -8930,6 +9039,7 @@ fn v16_attack_resolved_backing_withdraw_requires_full_user_wind_down() {
);
let vault_before = g.vault;
let dest_before = env.token_amount(dest);
let ledger = env.canonical_backing_domain_ledger_account(1);
env.svm.expire_blockhash();
let r = env.send(
ProgInstruction::WithdrawBackingBucket {
Expand All @@ -8943,6 +9053,7 @@ fn v16_attack_resolved_backing_withdraw_requires_full_user_wind_down() {
AccountMeta::new(env.vault, false),
AccountMeta::new_readonly(env.vault_authority, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(ledger, false),
],
&[&env.admin.insecure_clone()],
);
Expand Down
3 changes: 2 additions & 1 deletion tests/v16_fork_lp_vault_redeem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ fn execute_redemption_backing_state_matches_withdraw() {
let admin_a = env_a.admin.insecure_clone();
let src_a = Pubkey::new_unique();
set_token(&mut env_a.svm, src_a, env_a.collateral_mint, admin_a.pubkey(), 10_000_000);
let ledger_a = Pubkey::new_unique();
let (ledger_a, _) =
state::derive_lp_backing_ledger(&env_a.program_id, &env_a.market, DOMAIN);
env_a.svm.set_account(ledger_a, Account { lamports: 1_000_000_000, data: vec![0u8; state::backing_domain_ledger_account_len()], owner: env_a.program_id, executable: false, rent_epoch: 0 }).unwrap();
let dest_a = Pubkey::new_unique();
set_token(&mut env_a.svm, dest_a, env_a.collateral_mint, admin_a.pubkey(), 0);
Expand Down
Loading