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}