← Program logic

programs/bond_servicing/src/instructions/offer.rs

Source at revision e5ff2b3cb157, the source of the program deployed on devnet. SHA-256 of this file 05ce6105656462109d1262465a2845f91077d14382cbb50d79cbf322bfe57296

1//! Third corporate action: issuer buyback (first come, first served) and tender offer (pro rata).
2//! Design: docs/buyback-tender.md.
3//!   - An offer is fully funded in its own vault before anyone can sell or tender.
4//!   - Tendered bonds stay in the holder's account, locked by the transfer hook, so coupon rights at record
5//!     moments during the window stay with the holder.
6//!   - After the window, each tender has two separate outcomes. The rejected part is released by `resolve_tender`,
7//!     which needs no cash and no token accounts, so a blocked cash leg cannot keep it locked. The accepted part
8//!     stays committed until `settle_tender` buys it.
9//!   - Tenders are numbered as they are made and resolved in that order, so an oversubscribed offer allocates by
10//!     running total (`Offer::accept_next`): it buys exactly its maximum, and no tender can gain by being resolved at
11//!     a chosen moment.
12//!   - Bonds bought are burned and paid in one transaction, after saving the holder's and the bond's histories.
13
14use anchor_lang::prelude::*;
15use anchor_spl::associated_token::AssociatedToken;
16use anchor_spl::token_2022::Token2022;
17use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
18
19use crate::constants::*;
20use crate::error::BondError;
21use crate::events::{BondsBoughtBack, OfferCreated, OfferFunded, TenderChanged, TenderResolved};
22use crate::settlement::{burn_bonds, pay_cash, retire_quantity, Cash};
23use crate::state::{Bond, HolderRecord, Offer, OfferKind, Tender};
24
25// ---------------------------------------------------------------- create and fund
26
27#[derive(Accounts)]
28#[instruction(id: u16)]
29pub struct CreateOffer<'info> {
30    #[account(mut)]
31    pub registrar: Signer<'info>,
32
33    #[account(has_one = registrar, has_one = cash_mint)]
34    pub bond: Box<Account<'info, Bond>>,
35
36    #[account(
37        init,
38        payer = registrar,
39        space = 8 + Offer::INIT_SPACE,
40        seeds = [OFFER_SEED, bond.key().as_ref(), &id.to_le_bytes()],
41        bump,
42    )]
43    pub offer: Box<Account<'info, Offer>>,
44
45    pub cash_mint: Box<InterfaceAccount<'info, Mint>>,
46
47    #[account(
48        init,
49        payer = registrar,
50        seeds = [VAULT_SEED, offer.key().as_ref()],
51        bump,
52        token::mint = cash_mint,
53        token::authority = bond,
54        token::token_program = cash_token_program,
55    )]
56    pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
57
58    pub cash_token_program: Interface<'info, TokenInterface>,
59    pub system_program: Program<'info, System>,
60}
61
62pub fn handle_create_offer(
63    ctx: Context<CreateOffer>,
64    id: u16,
65    pro_rata: bool,
66    price_units: u64,
67    max_quantity: u64,
68    open_time: i64,
69    close_time: i64,
70) -> Result<()> {
71    let bond = &ctx.accounts.bond;
72    require!(
73        price_units > 0
74            && max_quantity > 0
75            && open_time < close_time
76            && close_time <= bond.final_record_time()?
77            && price_units.checked_mul(max_quantity).is_some(),
78        BondError::InvalidOffer
79    );
80    let offer = &mut ctx.accounts.offer;
81    offer.bond = bond.key();
82    offer.id = id;
83    offer.kind = if pro_rata { OfferKind::ProRata } else { OfferKind::FirstCome };
84    offer.price_units = price_units;
85    offer.max_quantity = max_quantity;
86    offer.open_time = open_time;
87    offer.close_time = close_time;
88    offer.total = 0;
89    offer.vault = ctx.accounts.vault.key();
90    offer.funded = false;
91    offer.tenders = 0;
92    offer.resolved = 0;
93    offer.resolved_quantity = 0;
94    offer.bump = ctx.bumps.offer;
95    emit!(OfferCreated { bond: bond.key(), offer: id, pro_rata, price_units, max_quantity, open_time, close_time });
96    Ok(())
97}
98
99#[derive(Accounts)]
100pub struct ConfirmOfferFunding<'info> {
101    #[account(mut, has_one = vault, seeds = [OFFER_SEED, offer.bond.as_ref(), &offer.id.to_le_bytes()], bump = offer.bump)]
102    pub offer: Box<Account<'info, Offer>>,
103
104    pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
105}
106
107pub fn handle_confirm_offer_funding(ctx: Context<ConfirmOfferFunding>) -> Result<()> {
108    let offer = &mut ctx.accounts.offer;
109    require!(!offer.funded, BondError::AlreadyFunded);
110    let budget = offer.budget().ok_or(BondError::Overflow)?;
111    let vault_balance = ctx.accounts.vault.amount;
112    require!(vault_balance >= budget, BondError::InsufficientFunding);
113    offer.funded = true;
114    emit!(OfferFunded { bond: offer.bond, offer: offer.id, vault_balance, budget });
115    Ok(())
116}
117
118// ---------------------------------------------------------------- shared accounts for retiring bonds against cash
119
120/// Accounts to burn a holder's bonds and pay from a vault. The offer is a separate account in each instruction, so
121/// each instruction takes it writable only if it changes it.
122#[derive(Accounts)]
123pub struct Retirement<'info> {
124    #[account(mut)]
125    pub payer: Signer<'info>,
126
127    #[account(mut, has_one = mint, has_one = cash_mint)]
128    pub bond: Box<Account<'info, Bond>>,
129
130    #[account(mut)]
131    pub mint: Box<InterfaceAccount<'info, Mint>>,
132
133    #[account(mut, has_one = bond, has_one = owner, seeds = [HOLDER_SEED, mint.key().as_ref(), owner.key().as_ref()], bump = holder.bump)]
134    pub holder: Box<Account<'info, HolderRecord>>,
135
136    /// CHECK: the holder's wallet, checked by `has_one = owner` on the holder record.
137    pub owner: UncheckedAccount<'info>,
138
139    #[account(
140        mut,
141        associated_token::mint = mint,
142        associated_token::authority = owner,
143        associated_token::token_program = token_program,
144    )]
145    pub holder_bonds: Box<InterfaceAccount<'info, TokenAccount>>,
146
147    #[account(mut)]
148    pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
149
150    pub cash_mint: Box<InterfaceAccount<'info, Mint>>,
151
152    #[account(
153        init_if_needed,
154        payer = payer,
155        associated_token::mint = cash_mint,
156        associated_token::authority = owner,
157        associated_token::token_program = cash_token_program,
158    )]
159    pub holder_cash: Box<InterfaceAccount<'info, TokenAccount>>,
160
161    /// CHECK: SPL Memo program; needed only when the holder's cash account requires memos (checked when used).
162    pub memo_program: Option<UncheckedAccount<'info>>,
163
164    pub token_program: Program<'info, Token2022>,
165    pub cash_token_program: Interface<'info, TokenInterface>,
166    pub associated_token_program: Program<'info, AssociatedToken>,
167    pub system_program: Program<'info, System>,
168}
169
170/// Burn `quantity` of the holder's bonds, pay quantity x price from the offer's vault, save histories and update
171/// quantities.
172fn buy_back(r: &mut Retirement, offer_id: u16, price_units: u64, quantity: u64) -> Result<u64> {
173    require!(r.holder_bonds.amount == r.holder.position.quantity, BondError::PositionMismatch);
174    let paid = quantity.checked_mul(price_units).ok_or(BondError::Overflow)?;
175    let passed = r.bond.passed_record_moments(r.bond.now()?)?;
176    let mint_key = r.bond.mint;
177    let bond_key = r.bond.key();
178    let bump = r.bond.bump;
179    let seeds: &[&[u8]] = &[BOND_SEED, mint_key.as_ref(), &[bump]];
180    let bond_info = r.bond.to_account_info();
181    burn_bonds(
182        &r.token_program.to_account_info(),
183        &r.holder_bonds.to_account_info(),
184        &r.mint.to_account_info(),
185        &bond_info,
186        quantity,
187        seeds,
188    )?;
189    let memo_info = r.memo_program.as_ref().map(|m| m.to_account_info());
190    pay_cash(
191        &Cash {
192            cash_token_program: &r.cash_token_program.to_account_info(),
193            vault: &r.vault.to_account_info(),
194            cash_mint: &r.cash_mint.to_account_info(),
195            holder_cash: &r.holder_cash.to_account_info(),
196            bond: &bond_info,
197            memo_program: memo_info.as_ref(),
198        },
199        paid,
200        r.cash_mint.decimals,
201        &format!("bond_servicing: offer {offer_id}"),
202        seeds,
203    )?;
204    retire_quantity(&mut r.holder, &mut r.bond, bond_key, passed, quantity)?;
205    emit!(BondsBoughtBack { bond: bond_key, offer: offer_id, owner: r.holder.owner, burned: quantity, paid });
206    Ok(paid)
207}
208
209// ---------------------------------------------------------------- buyback: sell at once
210
211#[derive(Accounts)]
212pub struct SellToOffer<'info> {
213    pub retirement: Retirement<'info>,
214
215    #[account(constraint = seller.key() == retirement.owner.key())]
216    pub seller: Signer<'info>,
217
218    #[account(
219        mut,
220        constraint = offer.bond == retirement.bond.key() && offer.vault == retirement.vault.key(),
221        seeds = [OFFER_SEED, retirement.bond.key().as_ref(), &offer.id.to_le_bytes()],
222        bump = offer.bump,
223    )]
224    pub offer: Box<Account<'info, Offer>>,
225}
226
227pub fn handle_sell_to_offer(ctx: Context<SellToOffer>, quantity: u64) -> Result<()> {
228    let a = &mut *ctx.accounts;
229    let now = a.retirement.bond.now()?;
230    require!(a.offer.kind == OfferKind::FirstCome, BondError::WrongOfferKind);
231    require!(a.offer.funded, BondError::OfferNotFunded);
232    require!(a.offer.open_time <= now && now < a.offer.close_time, BondError::OfferNotOpen);
233    let total = a.offer.total.checked_add(quantity).ok_or(BondError::Overflow)?;
234    require!(quantity > 0 && total <= a.offer.max_quantity, BondError::ExceedsOffer);
235    require!(quantity <= a.retirement.holder.unlocked(), BondError::InsufficientUnlocked);
236    a.offer.total = total;
237    buy_back(&mut a.retirement, a.offer.id, a.offer.price_units, quantity)?;
238    Ok(())
239}
240
241// ---------------------------------------------------------------- tender offer: tender, withdraw, resolve, settle
242
243#[derive(Accounts)]
244pub struct ChangeTender<'info> {
245    #[account(mut)]
246    pub owner: Signer<'info>,
247
248    pub bond: Box<Account<'info, Bond>>,
249
250    #[account(mut, has_one = bond, seeds = [OFFER_SEED, bond.key().as_ref(), &offer.id.to_le_bytes()], bump = offer.bump)]
251    pub offer: Box<Account<'info, Offer>>,
252
253    #[account(mut, has_one = bond, has_one = owner, seeds = [HOLDER_SEED, bond.mint.as_ref(), owner.key().as_ref()], bump = holder.bump)]
254    pub holder: Box<Account<'info, HolderRecord>>,
255
256    #[account(
257        init_if_needed,
258        payer = owner,
259        space = 8 + Tender::INIT_SPACE,
260        seeds = [TENDER_SEED, offer.key().as_ref(), owner.key().as_ref()],
261        bump,
262    )]
263    pub tender: Box<Account<'info, Tender>>,
264
265    pub system_program: Program<'info, System>,
266}
267
268/// Tender `quantity` more bonds (`withdraw` = false) or withdraw `quantity` (`withdraw` = true), while the window
269/// is open. Tendered bonds are locked in the holder's account.
270pub fn handle_change_tender(ctx: Context<ChangeTender>, quantity: u64, withdraw: bool) -> Result<()> {
271    let a = &mut *ctx.accounts;
272    let now = a.bond.now()?;
273    require!(a.offer.kind == OfferKind::ProRata, BondError::WrongOfferKind);
274    require!(a.offer.funded, BondError::OfferNotFunded);
275    require!(a.offer.open_time <= now && now < a.offer.close_time, BondError::OfferNotOpen);
276    require!(quantity > 0, BondError::ExceedsOffer);
277    let tender = &mut a.tender;
278    if tender.offer == Pubkey::default() {
279        tender.offer = a.offer.key();
280        tender.owner = a.owner.key();
281        tender.number = a.offer.tenders;
282        a.offer.tenders = a.offer.tenders.checked_add(1).ok_or(BondError::Overflow)?;
283        tender.quantity = 0;
284        tender.resolved = false;
285        tender.accepted = 0;
286        tender.settled = false;
287        tender.bump = ctx.bumps.tender;
288    }
289    if withdraw {
290        require!(quantity <= tender.quantity, BondError::ExceedsOffer);
291        tender.quantity -= quantity;
292        a.holder.locked -= quantity;
293        a.offer.total -= quantity;
294    } else {
295        require!(quantity <= a.holder.unlocked(), BondError::InsufficientUnlocked);
296        tender.quantity = tender.quantity.checked_add(quantity).ok_or(BondError::Overflow)?;
297        a.holder.locked = a.holder.locked.checked_add(quantity).ok_or(BondError::Overflow)?;
298        a.offer.total = a.offer.total.checked_add(quantity).ok_or(BondError::Overflow)?;
299    }
300    emit!(TenderChanged {
301        bond: a.bond.key(),
302        offer: a.offer.id,
303        owner: a.owner.key(),
304        tendered: tender.quantity,
305        offer_total: a.offer.total,
306    });
307    Ok(())
308}
309
310/// After the window: fix the accepted part (everything if the offer is not oversubscribed, else the running-total
311/// pro-rata share, `Offer::accept_next`) and release the rejected part. Tenders are resolved in the order they were
312/// made. Idempotent; a resolved tender is left as it is.
313fn resolve(bond_key: Pubkey, offer: &mut Offer, tender: &mut Tender, holder: &mut HolderRecord) -> Result<()> {
314    if tender.resolved {
315        return Ok(());
316    }
317    require!(tender.number == offer.resolved, BondError::TenderOutOfOrder);
318    let accepted = offer.accept_next(tender.quantity).ok_or(BondError::Overflow)?;
319    let rejected = tender.quantity - accepted;
320    holder.locked = holder.locked.checked_sub(rejected).ok_or(BondError::Overflow)?;
321    tender.accepted = accepted;
322    tender.resolved = true;
323    emit!(TenderResolved { bond: bond_key, offer: offer.id, owner: holder.owner, accepted, rejected });
324    Ok(())
325}
326
327#[derive(Accounts)]
328pub struct ResolveTender<'info> {
329    pub bond: Box<Account<'info, Bond>>,
330
331    #[account(mut, has_one = bond, seeds = [OFFER_SEED, bond.key().as_ref(), &offer.id.to_le_bytes()], bump = offer.bump)]
332    pub offer: Box<Account<'info, Offer>>,
333
334    #[account(mut, has_one = bond, seeds = [HOLDER_SEED, bond.mint.as_ref(), holder.owner.as_ref()], bump = holder.bump)]
335    pub holder: Box<Account<'info, HolderRecord>>,
336
337    #[account(mut, seeds = [TENDER_SEED, offer.key().as_ref(), holder.owner.as_ref()], bump = tender.bump)]
338    pub tender: Box<Account<'info, Tender>>,
339}
340
341/// Anyone, after the window: release the rejected part of one holder's tender. Needs no cash and no token
342/// accounts, so it works even when the offer's cash cannot move.
343pub fn handle_resolve_tender(ctx: Context<ResolveTender>) -> Result<()> {
344    let a = &mut *ctx.accounts;
345    require!(a.offer.kind == OfferKind::ProRata, BondError::WrongOfferKind);
346    require!(a.bond.now()? >= a.offer.close_time, BondError::OfferStillOpen);
347    resolve(a.bond.key(), &mut a.offer, &mut a.tender, &mut a.holder)
348}
349
350#[derive(Accounts)]
351pub struct SettleTender<'info> {
352    pub retirement: Retirement<'info>,
353
354    /// Writable: settling resolves the tender first if nobody has (the offer counts resolutions).
355    #[account(
356        mut,
357        constraint = offer.bond == retirement.bond.key() && offer.vault == retirement.vault.key(),
358        seeds = [OFFER_SEED, retirement.bond.key().as_ref(), &offer.id.to_le_bytes()],
359        bump = offer.bump,
360    )]
361    pub offer: Box<Account<'info, Offer>>,
362
363    #[account(
364        mut,
365        seeds = [TENDER_SEED, offer.key().as_ref(), retirement.owner.key().as_ref()],
366        bump = tender.bump,
367    )]
368    pub tender: Box<Account<'info, Tender>>,
369}
370
371/// Anyone, after the window: buy the accepted part of one holder's tender (resolving it first if nobody has), burn
372/// and pay in one transaction, and release it from the lock.
373pub fn handle_settle_tender(ctx: Context<SettleTender>) -> Result<()> {
374    let a = &mut *ctx.accounts;
375    require!(a.offer.kind == OfferKind::ProRata, BondError::WrongOfferKind);
376    require!(a.retirement.bond.now()? >= a.offer.close_time, BondError::OfferStillOpen);
377    resolve(a.retirement.bond.key(), &mut a.offer, &mut a.tender, &mut a.retirement.holder)?;
378    require!(!a.tender.settled, BondError::AlreadySettled);
379    let accepted = a.tender.accepted;
380    a.retirement.holder.locked = a.retirement.holder.locked.checked_sub(accepted).ok_or(BondError::Overflow)?;
381    a.tender.settled = true;
382    if accepted > 0 {
383        buy_back(&mut a.retirement, a.offer.id, a.offer.price_units, accepted)?;
384    }
385    Ok(())
386}