← Bond stages

programs/bond_servicing/src/state.rs

Source snapshot · revision 5e22d3ea5fa4.

SHA-256 e14705ac356391a065cd73cdc842f170f1f27a422a7d037d47eadd2683c864fc

1use anchor_lang::prelude::*;
2use bond_math::amount::{coupon_amount, fixed_coupon_amount, principal_amount, AccrualBasis};
3use bond_math::date::Date;
4use bond_math::daycount::Thirty360;
5use bond_math::time::{chain_to_bond_time, SECONDS_PER_DAY};
6
7use crate::constants::MAX_PERIODS;
8use crate::error::BondError;
9
10/// How a coupon is computed (see `bond_math::amount::AccrualBasis`).
11#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
12pub enum CouponBasis {
13    /// Rate / periods per year for every regular period.
14    FixedPerPeriod { periods_per_year: u32 },
15    /// 30/360 accrual: 0 = BondBasis, 1 = European, 2 = USA.
16    Thirty360 { convention: u8 },
17}
18
19#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
20pub enum EventKind {
21    Coupon,
22    Redemption,
23}
24
25/// A quantity over the bond's record moments. Record moments are numbered 0..periods in time order.
26///
27/// `quantity` is the quantity now. It has not changed since the last change, which happened when `live_from`
28/// record moments had passed. So the quantity at record moment `e` is:
29///   - `at[e]` if `e < live_from` (saved by the first change after moment `e`);
30///   - `quantity` otherwise (no change since moment `e`).
31/// `save` must run before every change; then no record moment's quantity is ever lost or overwritten, however
32/// late anyone looks.
33#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
34pub struct PositionHistory {
35    pub quantity: u64,
36    pub live_from: u16,
37    pub at: [u64; MAX_PERIODS],
38}
39
40impl PositionHistory {
41    /// A new position created when `passed` record moments have passed: zero at all of them.
42    pub fn new(passed: u16) -> Self {
43        Self { quantity: 0, live_from: passed, at: [0; MAX_PERIODS] }
44    }
45
46    /// Keep the current quantity for every record moment passed since the last change. Returns the saved
47    /// range of record moments, if any.
48    pub fn save(&mut self, passed: u16) -> Option<(u16, u16)> {
49        if passed <= self.live_from {
50            return None;
51        }
52        let from = self.live_from;
53        for e in from..passed {
54            self.at[e as usize] = self.quantity;
55        }
56        self.live_from = passed;
57        Some((from, passed))
58    }
59
60    /// Quantity at record moment `e`; valid once moment `e` has passed.
61    pub fn at_moment(&self, e: u16) -> u64 {
62        if e < self.live_from {
63            self.at[e as usize]
64        } else {
65            self.quantity
66        }
67    }
68}
69
70#[account]
71#[derive(InitSpace)]
72pub struct Bond {
73    pub registrar: Pubkey,
74    pub mint: Pubkey,
75    pub cash_mint: Pubkey,
76    /// Face value of one bond in cash-token base units.
77    pub face_units: u64,
78    pub rate_bps: u32,
79    pub basis: CouponBasis,
80    /// Redemption price in basis points of face (10_000 = par).
81    pub redemption_price_bps: u32,
82    /// Day serials (days since 1970-01-01).
83    pub issue_day: i32,
84    pub maturity_day: i32,
85    pub months_per_period: u32,
86    /// Business-day-adjusted payment day of each coupon period (computed off-chain with the bond's calendar).
87    #[max_len(MAX_PERIODS)]
88    pub payment_days: Vec<i32>,
89    /// Record day of each coupon period; the record moment is `record_seconds_local` on that day.
90    #[max_len(MAX_PERIODS)]
91    pub record_days: Vec<i32>,
92    pub record_seconds_local: i32,
93    /// Payment opens at this local time on the payment day (never before the record moment).
94    pub payment_seconds_local: i32,
95    pub utc_offset_seconds: i32,
96    /// Time model: bond time = bond_origin + (chain time - chain_origin) * 86_400 / chain_seconds_per_bond_day.
97    pub chain_origin: i64,
98    pub bond_origin: i64,
99    pub chain_seconds_per_bond_day: i64,
100    /// Bond supply over record moments. Only the program changes supply (placement, redemption), and it saves
101    /// the history first, so the supply at any record moment is known without counting holders.
102    pub supply: PositionHistory,
103    /// Issue size in bonds, fixed by the terms: placement never takes the supply above it.
104    pub max_supply: u64,
105    pub bump: u8,
106}
107
108impl Bond {
109    pub fn periods(&self) -> u16 {
110        self.payment_days.len() as u16
111    }
112
113    /// Index of the redemption event; coupon events are 0..periods.
114    pub fn redemption_event(&self) -> u16 {
115        self.periods()
116    }
117
118    /// The coupon period an event belongs to: the redemption shares the final period's dates.
119    pub fn period_of(&self, event: u16) -> Result<u16> {
120        require!(event <= self.redemption_event(), BondError::EventOutOfRange);
121        Ok(if event == self.redemption_event() { event - 1 } else { event })
122    }
123
124    pub fn now(&self) -> Result<i64> {
125        let chain_now = Clock::get()?.unix_timestamp;
126        chain_to_bond_time(chain_now, self.chain_origin, self.bond_origin, self.chain_seconds_per_bond_day)
127            .ok_or_else(|| error!(BondError::Overflow))
128    }
129
130    fn local_moment(&self, day: i32, seconds_local: i32) -> i64 {
131        i64::from(day) * SECONDS_PER_DAY + i64::from(seconds_local) - i64::from(self.utc_offset_seconds)
132    }
133
134    /// Record moment (bond time) of an event.
135    pub fn record_time(&self, event: u16) -> Result<i64> {
136        let period = self.period_of(event)?;
137        Ok(self.local_moment(self.record_days[period as usize], self.record_seconds_local))
138    }
139
140    /// Moment (bond time) from which an event may be paid.
141    pub fn payment_time(&self, event: u16) -> Result<i64> {
142        let period = self.period_of(event)?;
143        Ok(self.local_moment(self.payment_days[period as usize], self.payment_seconds_local))
144    }
145
146    /// Trading closes at the final record moment: balances are frozen for redemption.
147    pub fn final_record_time(&self) -> Result<i64> {
148        self.record_time(self.periods() - 1)
149    }
150
151    /// Number of record moments at or before `bond_now`.
152    pub fn passed_record_moments(&self, bond_now: i64) -> Result<u16> {
153        self.passed_record_moments_from(bond_now, 0)
154    }
155
156    /// Holder-local cursors provide a period hint; no shared mutable epoch cache.
157    pub fn passed_record_moments_from(&self, bond_now: i64, hint: u16) -> Result<u16> {
158        let mut lo = usize::from(hint).min(self.record_days.len());
159        // Preserve exact lookup even if a supplied hint is ahead of the queried time.
160        if lo > 0 && self.local_moment(self.record_days[lo - 1], self.record_seconds_local) > bond_now { lo = 0; }
161        if lo == self.record_days.len() || self.local_moment(self.record_days[lo], self.record_seconds_local) > bond_now {
162            return Ok(lo as u16);
163        }
164        let passed = self.record_days[lo..].partition_point(|day| self.local_moment(*day, self.record_seconds_local) <= bond_now);
165        Ok((lo + passed) as u16)
166    }
167
168    /// Unadjusted accrual dates of coupon period `i` (regular schedule, counted back from maturity).
169    pub fn accrual_dates(&self, i: u16) -> (Date, Date) {
170        let maturity = Date::from_days(i64::from(self.maturity_day));
171        let back = i32::from(self.periods() - i);
172        let step = self.months_per_period as i32;
173        (maturity.add_months(-back * step), maturity.add_months(-(back - 1) * step))
174    }
175
176    pub fn accrual_basis(&self) -> Result<AccrualBasis> {
177        Ok(match self.basis {
178            CouponBasis::FixedPerPeriod { periods_per_year } => AccrualBasis::FixedPerPeriod { periods_per_year },
179            CouponBasis::Thirty360 { convention } => AccrualBasis::DayCount(match convention {
180                0 => Thirty360::BondBasis,
181                1 => Thirty360::European,
182                2 => Thirty360::Usa,
183                _ => return err!(BondError::InvalidTerms),
184            }),
185        })
186    }
187
188    /// Coupon on a holding of `position` bonds for coupon period `period`, rounded down once.
189    pub fn coupon(&self, position: u64, period: u16) -> Result<u64> {
190        let amount = match self.basis {
191            CouponBasis::FixedPerPeriod { periods_per_year } => fixed_coupon_amount(position, self.face_units, self.rate_bps, periods_per_year),
192            _ => { let (start, end) = self.accrual_dates(period);
193                coupon_amount(position, self.face_units, self.rate_bps, self.accrual_basis()?, start, end) }
194        };
195        amount.ok_or_else(|| error!(BondError::Overflow))
196    }
197
198    pub fn principal(&self, position: u64) -> Result<u64> {
199        principal_amount(position, self.face_units, self.redemption_price_bps).ok_or_else(|| error!(BondError::Overflow))
200    }
201
202    /// Every amount the bond can owe on `supply` bonds (each coupon budget and the redemption budget) fits the cash
203    /// token's u64 amounts. Checked when supply grows (placement); supply only shrinks afterwards.
204    pub fn check_serviceable(&self, supply: u64) -> Result<()> {
205        let basis = self.accrual_basis()?;
206        match basis {
207            AccrualBasis::FixedPerPeriod { periods_per_year } => {
208                fixed_coupon_amount(supply, self.face_units, self.rate_bps, periods_per_year).ok_or(BondError::IssueTooLarge)?;
209            }
210            _ => { for period in 0..self.periods() {
211                let (start, end) = self.accrual_dates(period);
212                coupon_amount(supply, self.face_units, self.rate_bps, basis, start, end).ok_or(BondError::IssueTooLarge)?;
213            } }
214        }
215        principal_amount(supply, self.face_units, self.redemption_price_bps).ok_or(BondError::IssueTooLarge)?;
216        Ok(())
217    }
218}
219
220/// One registered holder of one bond: its quantity over record moments and the coupons already paid.
221#[account]
222#[derive(InitSpace)]
223pub struct HolderRecord {
224    pub bond: Pubkey,
225    pub owner: Pubkey,
226    pub position: PositionHistory,
227    /// Bit `e` set = coupon `e` paid.
228    pub paid: u64,
229    /// Bonds committed to offers: tendered into open offers, or accepted by a closed offer and not yet bought. They
230    /// stay in the holder's account (and keep their coupon rights), but the transfer hook refuses transfers that
231    /// would take the balance below this, and redemption leaves them to their offer.
232    pub locked: u64,
233    /// Bonds redeemed at maturity so far. Principal for each partial redemption is the difference of the principal
234    /// on the cumulative quantity, so several partial redemptions round exactly like one.
235    pub redeemed: u64,
236    pub bump: u8,
237}
238
239impl HolderRecord {
240    pub fn is_paid(&self, event: u16) -> bool {
241        self.paid & (1u64 << event) != 0
242    }
243
244    pub fn unlocked(&self) -> u64 {
245        self.position.quantity.saturating_sub(self.locked)
246    }
247}
248
249#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
250pub enum OfferKind {
251    /// Buyback: holders sell at once, first come first served, until the maximum is bought.
252    FirstCome,
253    /// Tender offer: holders tender during the window; after it, pro rata if oversubscribed.
254    ProRata,
255}
256
257/// An issuer's offer to buy back bonds at a price per bond, up to a maximum quantity, during a window.
258#[account]
259#[derive(InitSpace)]
260pub struct Offer {
261    pub bond: Pubkey,
262    pub id: u16,
263    pub kind: OfferKind,
264    /// All-in cash price per bond, in cash-token base units.
265    pub price_units: u64,
266    pub max_quantity: u64,
267    /// Window in bond time: open <= t < close.
268    pub open_time: i64,
269    pub close_time: i64,
270    /// Bonds bought (FirstCome) or currently tendered (ProRata).
271    pub total: u64,
272    /// The offer's own cash account.
273    pub vault: Pubkey,
274    pub funded: bool,
275    /// Tenders made (ProRata): each new tender takes the next number.
276    pub tenders: u32,
277    /// Tenders resolved so far, in the order they were made.
278    pub resolved: u32,
279    /// Bonds tendered by the tenders resolved so far.
280    pub resolved_quantity: u64,
281    pub bump: u8,
282}
283
284impl Offer {
285    /// Cash that covers the whole offer.
286    pub fn budget(&self) -> Option<u64> {
287        self.price_units.checked_mul(self.max_quantity)
288    }
289
290    /// Bonds accepted from the next tender of `quantity`, once the window has closed. Not oversubscribed: all of it.
291    /// Oversubscribed: tenders are taken in the order they were made, and each receives the pro-rata share of all
292    /// tenders up to and including it, rounded down, minus that of the tenders before it. The shares telescope, so the
293    /// offer buys exactly its maximum; each tender receives at least its own pro-rata share rounded down, never more
294    /// than it tendered, and differs from its exact share by less than one bond.
295    pub fn accept_next(&mut self, quantity: u64) -> Option<u64> {
296        let before = self.resolved_quantity;
297        let after = before.checked_add(quantity)?;
298        let accepted = if self.total <= self.max_quantity {
299            quantity
300        } else {
301            let share = |q: u64| (u128::from(q) * u128::from(self.max_quantity) / u128::from(self.total)) as u64;
302            share(after) - share(before)
303        };
304        self.resolved_quantity = after;
305        self.resolved = self.resolved.checked_add(1)?;
306        Some(accepted)
307    }
308}
309
310/// One holder's tender into one pro-rata offer.
311#[account]
312#[derive(InitSpace)]
313pub struct Tender {
314    pub offer: Pubkey,
315    pub owner: Pubkey,
316    /// Its number in the offer: tenders are resolved in this order.
317    pub number: u32,
318    pub quantity: u64,
319    /// After the window: the accepted part is fixed and the rejected part released (no cash involved).
320    pub resolved: bool,
321    pub accepted: u64,
322    /// The accepted part has been bought (burned and paid).
323    pub settled: bool,
324    pub bump: u8,
325}
326
327/// One coupon or the redemption. Created once its record moment has passed, so its inputs are final.
328/// Read-only on the payment path.
329#[account]
330#[derive(InitSpace)]
331pub struct Event {
332    pub bond: Pubkey,
333    pub index: u16,
334    pub kind: EventKind,
335    /// Record moment and payment opening, in bond time.
336    pub record_time: i64,
337    pub payment_time: i64,
338    /// Bond supply at the record moment (from the bond's supply history).
339    pub supply_at_record: u64,
340    /// Cash that covers every holder of this event: the amount for the whole supply, rounded down once. Each
341    /// holder's amount is rounded down on its own, so the holders' amounts never add up to more than this.
342    pub budget: u64,
343    /// This event's own cash account; other events cannot spend it.
344    pub vault: Pubkey,
345    pub funded: bool,
346    pub bump: u8,
347}