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
7 changes: 7 additions & 0 deletions finance/options/anchor-v1/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 2026-09-30

Rename the market's `underlying_locked` and `quote_locked` to `underlying_owed`
and `quote_owed`. Each counts what the vault owes, to writers as collateral and
to writers and holders as settlement proceeds waiting to be collected, and
"locked" did not say to whom. The account layout is unchanged.

## 2026-09-10

The `Market` account is now the token authority of both vaults and signs
Expand Down
4 changes: 2 additions & 2 deletions finance/options/anchor-v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ insurance he did not need; Maria earned 1% of every premium.
## Custody

The two vaults hold other people's money, so the `Market` account keeps a
ledger of what each vault owes: `underlying_locked` (call collateral, plus put
holders' deliveries awaiting collection), `quote_locked` (put collateral, plus
ledger of what each vault owes: `underlying_owed` (call collateral, plus put
holders' deliveries awaiting collection), `quote_owed` (put collateral, plus
call holders' strike payments awaiting collection) and `fees_owed`. Every
handler that moves tokens updates the ledger before any transfer and then
asserts that each vault still covers what it owes (`CustodyInvariantViolated`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn handle_buy_option(context: Context<BuyOptionAccountConstraints>) -> Resul
.amount
.checked_add(fee)
.ok_or(OptionsError::MathOverflow)?;
check_custody(market, market.underlying_locked, quote_after)?;
check_custody(market, market.underlying_owed, quote_after)?;

transfer_from_signer(
&context.accounts.token_program,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ pub fn handle_cancel_option(context: Context<CancelOptionAccountConstraints>) ->
let mut quote_after = context.accounts.quote_vault.amount;
match kind {
OptionKind::Call => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
underlying_after = underlying_after
.checked_sub(collateral)
.ok_or(OptionsError::CustodyInvariantViolated)?;
}
OptionKind::Put => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
quote_after = quote_after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::state::Market;

/// The admin sweeps the fees the venue has earned on premiums. `fees_owed`
/// is the only part of the quote vault the admin can reach: the collateral
/// and strike payments beside it are locked to their writers and holders.
/// and strike payments beside it are owed to their writers and holders.
pub fn handle_collect_fees(context: Context<CollectFeesAccountConstraints>) -> Result<()> {
let market = &mut context.accounts.market;
let amount = market.fees_owed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub fn handle_collect_proceeds(context: Context<CollectProceedsAccountConstraint
match kind {
// A call's proceeds are the strike, in the quote token.
OptionKind::Call => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_sub(proceeds)
.ok_or(OptionsError::MathOverflow)?;
quote_after = quote_after
Expand All @@ -45,8 +45,8 @@ pub fn handle_collect_proceeds(context: Context<CollectProceedsAccountConstraint
}
// A put's proceeds are the delivered underlying.
OptionKind::Put => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_sub(proceeds)
.ok_or(OptionsError::MathOverflow)?;
underlying_after = underlying_after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ pub fn handle_exercise_option(context: Context<ExerciseOptionAccountConstraints>
let quote_before = context.accounts.quote_vault.amount;
let (underlying_after, quote_after) = match kind {
OptionKind::Call => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_sub(underlying_total)
.ok_or(OptionsError::MathOverflow)?;
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_add(strike_total)
.ok_or(OptionsError::MathOverflow)?;
(
Expand All @@ -66,12 +66,12 @@ pub fn handle_exercise_option(context: Context<ExerciseOptionAccountConstraints>
)
}
OptionKind::Put => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_sub(strike_total)
.ok_or(OptionsError::MathOverflow)?;
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_add(underlying_total)
.ok_or(OptionsError::MathOverflow)?;
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ pub fn handle_initialize_market(
market.quote_mint = context.accounts.quote_mint.key();
market.underlying_vault = context.accounts.underlying_vault.key();
market.quote_vault = context.accounts.quote_vault.key();
market.underlying_locked = 0;
market.quote_locked = 0;
market.underlying_owed = 0;
market.quote_owed = 0;
market.fees_owed = 0;
market.fee_bps = fee_bps;
market.bump = context.bumps.market;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ pub fn handle_reclaim_collateral(
let mut quote_after = context.accounts.quote_vault.amount;
match kind {
OptionKind::Call => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
underlying_after = underlying_after
.checked_sub(collateral)
.ok_or(OptionsError::CustodyInvariantViolated)?;
}
OptionKind::Put => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
quote_after = quote_after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ use crate::state::Market;
/// behind, computed from the balances read before any CPI ran.
pub fn check_custody(market: &Market, underlying_after: u64, quote_after: u64) -> Result<()> {
require!(
underlying_after >= market.underlying_locked,
underlying_after >= market.underlying_owed,
OptionsError::CustodyInvariantViolated
);
let quote_owed = market
.quote_locked
.quote_owed
.checked_add(market.fees_owed)
.ok_or(OptionsError::MathOverflow)?;
require!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,17 @@ pub fn handle_write_option(
let mut quote_after = context.accounts.quote_vault.amount;
match kind {
OptionKind::Call => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_add(collateral)
.ok_or(OptionsError::MathOverflow)?;
underlying_after = underlying_after
.checked_add(collateral)
.ok_or(OptionsError::MathOverflow)?;
}
OptionKind::Put => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_add(collateral)
.ok_or(OptionsError::MathOverflow)?;
quote_after = quote_after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ pub struct Market {

/// Underlying minor units the vault owes: call writers' collateral, plus
/// put holders' deliveries awaiting the writer's `collect_proceeds`.
pub underlying_locked: u64,
pub underlying_owed: u64,

/// Quote minor units the vault owes: put writers' collateral, plus call
/// holders' strike payments awaiting the writer's `collect_proceeds`.
pub quote_locked: u64,
pub quote_owed: u64,

/// Quote minor units held in the quote vault for the admin, accrued from
/// the fee on each premium and swept by `collect_fees`.
Expand Down
34 changes: 17 additions & 17 deletions finance/options/anchor-v1/programs/options/tests/test_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,13 +477,13 @@ impl Venue {
let market = self.market_state();
assert_eq!(
self.balance(&self.underlying_vault),
market.underlying_locked,
"underlying vault must hold exactly the locked underlying"
market.underlying_owed,
"underlying vault must hold exactly the underlying owed"
);
assert_eq!(
self.balance(&self.quote_vault),
market.quote_locked + market.fees_owed,
"quote vault must hold exactly the locked quote plus the fees owed"
market.quote_owed + market.fees_owed,
"quote vault must hold exactly the quote owed plus the fees owed"
);
}
}
Expand Down Expand Up @@ -521,7 +521,7 @@ fn test_write_call_moves_underlying_into_vault() {
assert_eq!(state.contracts, CONTRACTS);
assert_eq!(state.strike_per_contract, CALL_STRIKE);
assert_eq!(state.premium, CALL_PREMIUM);
assert_eq!(venue.market_state().underlying_locked, FIVE_NVDAX);
assert_eq!(venue.market_state().underlying_owed, FIVE_NVDAX);
venue.assert_vaults_match_ledger();
}

Expand Down Expand Up @@ -575,8 +575,8 @@ fn test_exercise_call_swaps_the_strike_for_the_underlying() {
assert_eq!(venue.balance(&venue.underlying_vault), 0);
assert_eq!(venue.balance(&venue.quote_vault), strike_total + 250_000);
let market = venue.market_state();
assert_eq!(market.underlying_locked, 0);
assert_eq!(market.quote_locked, strike_total);
assert_eq!(market.underlying_owed, 0);
assert_eq!(market.quote_owed, strike_total);
assert_eq!(venue.option_state(&option).status, OptionStatus::Exercised);
venue.assert_vaults_match_ledger();
}
Expand Down Expand Up @@ -609,13 +609,13 @@ fn test_collect_proceeds_pays_the_writer_and_closes_the_option() {
"the option's rent must return to the writer"
);
let market = venue.market_state();
assert_eq!(market.quote_locked, 0);
assert_eq!(market.quote_owed, 0);
assert_eq!(market.fees_owed, 250_000);
venue.assert_vaults_match_ledger();
}

/// Maria sweeps the venue's fee. Only the 0.25 USDC of fees leaves the
/// vault; the strike payment sitting beside it stays locked to Alice.
/// vault; the strike payment sitting beside it stays owed to Alice.
#[test]
fn test_collect_fees_pays_only_the_fees_owed() {
let mut venue = Venue::new();
Expand Down Expand Up @@ -657,7 +657,7 @@ fn test_put_lifecycle_delivers_the_underlying_for_the_strike() {
let collateral = 750 * ONE_TOKEN;
assert_eq!(venue.balance(&carol.quote), STANDARD_USDC - collateral);
assert_eq!(venue.balance(&venue.quote_vault), collateral);
assert_eq!(venue.market_state().quote_locked, collateral);
assert_eq!(venue.market_state().quote_owed, collateral);
venue.assert_vaults_match_ledger();

venue.buy_option(&dave, &carol.pubkey(), &option).unwrap();
Expand All @@ -680,13 +680,13 @@ fn test_put_lifecycle_delivers_the_underlying_for_the_strike() {
assert_eq!(venue.balance(&venue.underlying_vault), FIVE_NVDAX);
assert_eq!(venue.balance(&venue.quote_vault), fee);
let market = venue.market_state();
assert_eq!(market.underlying_locked, FIVE_NVDAX);
assert_eq!(market.quote_locked, 0);
assert_eq!(market.underlying_owed, FIVE_NVDAX);
assert_eq!(market.quote_owed, 0);
venue.assert_vaults_match_ledger();

venue.collect_proceeds(&carol, &option).unwrap();
assert_eq!(venue.balance(&carol.underlying), FIVE_NVDAX);
assert_eq!(venue.market_state().underlying_locked, 0);
assert_eq!(venue.market_state().underlying_owed, 0);
assert!(!venue.option_exists(&option));
venue.assert_vaults_match_ledger();
}
Expand All @@ -713,7 +713,7 @@ fn test_reclaim_collateral_after_expiry_returns_it_to_the_writer() {
);
assert_eq!(venue.balance(&bob.quote), STANDARD_USDC - CALL_PREMIUM);
assert!(!venue.option_exists(&option));
assert_eq!(venue.market_state().underlying_locked, 0);
assert_eq!(venue.market_state().underlying_owed, 0);
venue.assert_vaults_match_ledger();
}

Expand Down Expand Up @@ -781,8 +781,8 @@ fn test_buy_is_refused_after_expiry() {
// ===========================================================================

/// An unsold option can be withdrawn at any time, collateral back, account
/// closed. Without this, an option nobody buys would lock the writer's tokens
/// forever.
/// closed. Without this, an option nobody buys would leave the writer's tokens
/// in the vault forever.
#[test]
fn test_cancel_unsold_option_returns_the_collateral() {
let mut venue = Venue::new();
Expand Down Expand Up @@ -810,7 +810,7 @@ fn test_cancel_unsold_option_works_after_expiry() {
venue.cancel_option(&carol, &option).unwrap();

assert_eq!(venue.balance(&carol.quote), STANDARD_USDC);
assert_eq!(venue.market_state().quote_locked, 0);
assert_eq!(venue.market_state().quote_owed, 0);
venue.assert_vaults_match_ledger();
}

Expand Down
7 changes: 7 additions & 0 deletions finance/options/anchor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 2026-09-30

Rename the market's `underlying_locked` and `quote_locked` to `underlying_owed`
and `quote_owed`. Each counts what the vault owes, to writers as collateral and
to writers and holders as settlement proceeds waiting to be collected, and
"locked" did not say to whom. The account layout is unchanged.

## 2026-09-10

The `Market` account is now the token authority of both vaults and signs
Expand Down
6 changes: 3 additions & 3 deletions finance/options/anchor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ insurance he did not need; Maria earned 1% of every premium.
## Custody

The two vaults hold other people's money, so the `Market` account keeps a
ledger of what each vault owes: `underlying_locked` (call collateral, plus put
holders' deliveries awaiting collection), `quote_locked` (put collateral, plus
ledger of what each vault owes: `underlying_owed` (call collateral, plus put
holders' deliveries awaiting collection), `quote_owed` (put collateral, plus
call holders' strike payments awaiting collection) and `fees_owed`. Every
handler that moves tokens updates the ledger before any transfer and then
asserts that each vault still covers what it owes (`CustodyInvariantViolated`
Expand Down Expand Up @@ -248,5 +248,5 @@ exercise.

`buy_option` takes `fee_bps` of every premium into the quote vault, and the
admin sweeps it with `collect_fees`. The fee is the admin's only reach into
the vault; collateral and strike payments are locked to their writers and
the vault; collateral and strike payments are owed to their writers and
holders.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn handle_buy_option(context: &mut Context<BuyOptionAccountConstraints>) ->
.amount()
.checked_add(fee)
.ok_or(OptionsError::MathOverflow)?;
check_custody(market, market.underlying_locked, quote_after)?;
check_custody(market, market.underlying_owed, quote_after)?;

transfer_from_signer(
&context.accounts.token_program,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ pub fn handle_cancel_option(context: &mut Context<CancelOptionAccountConstraints
let mut quote_after = context.accounts.quote_vault.amount();
match kind {
OptionKind::Call => {
market.underlying_locked = market
.underlying_locked
market.underlying_owed = market
.underlying_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
underlying_after = underlying_after
.checked_sub(collateral)
.ok_or(OptionsError::CustodyInvariantViolated)?;
}
OptionKind::Put => {
market.quote_locked = market
.quote_locked
market.quote_owed = market
.quote_owed
.checked_sub(collateral)
.ok_or(OptionsError::MathOverflow)?;
quote_after = quote_after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::state::Market;

/// The admin sweeps the fees the venue has earned on premiums. `fees_owed`
/// is the only part of the quote vault the admin can reach: the collateral
/// and strike payments beside it are locked to their writers and holders.
/// and strike payments beside it are owed to their writers and holders.
pub fn handle_collect_fees(context: &mut Context<CollectFeesAccountConstraints>) -> Result<()> {
let market = &mut context.accounts.market;
let amount = market.fees_owed;
Expand Down
Loading
Loading