Skip to content

Commit 61e25c5

Browse files
author
Edward (OpenClaw)
committed
asset-leasing: merge accounts section into lifecycle
WHAT: Deleted the standalone Accounts and program-derived addresses section and integrated its content into the Lifecycle handler walkthroughs. Each account is now introduced at the moment it is first created or first used: - Lease, leased_vault, collateral_vault: introduced in create_lease with seeds and roles in a new program-derived addresses bullet list, plus the Lease struct definition in a new What's on the lease account subsection. - holder, short_seller, payer, keeper user wallets: a one-line description added on first appearance in their respective handler's Signers bullet. - Associated token accounts (holder_leased_account, holder_collateral_account, short_seller_*, keeper_collateral_account): one-line associated-token-account description added on first appearance in each handler's Accounts bullet. - price_update: introduced in liquidate, where the Pyth oracle account is first passed in. - The Closed/Liquidated states paragraph moved to the end of return_lease (the first close handler), since it covers all three closing paths. Table of contents and any cross-references to the deleted section updated. WHY: A reader walking the lifecycle no longer has to flip back to a static account dump to know what each account is - the prose introduces every account the moment it appears in the narrative.
1 parent ab99204 commit 61e25c5

1 file changed

Lines changed: 97 additions & 102 deletions

File tree

defi/asset-leasing/anchor/README.md

Lines changed: 97 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ need to sell short. The program is written in
2424
## Table of contents
2525

2626
1. [What does this program do?](#what-does-this-program-do)
27-
2. [Accounts and program-derived addresses](#accounts-and-program-derived-addresses)
28-
3. [Lifecycle](#lifecycle)
29-
4. [Safety and edge cases](#safety-and-edge-cases)
30-
5. [Running the tests](#running-the-tests)
31-
6. [Quasar port](#quasar-port)
32-
7. [Extending the program](#extending-the-program)
27+
2. [Lifecycle](#lifecycle)
28+
3. [Safety and edge cases](#safety-and-edge-cases)
29+
4. [Running the tests](#running-the-tests)
30+
5. [Quasar port](#quasar-port)
31+
6. [Extending the program](#extending-the-program)
3332

3433
---
3534

@@ -205,80 +204,6 @@ above is the same machinery applied to a real asset pair.
205204

206205
---
207206

208-
## Accounts and program-derived addresses
209-
210-
Every call to the program touches some subset of these accounts. The
211-
three [program-derived addresses](https://solana.com/docs/terminology)
212-
are created on `create_lease` and destroyed on `return_lease` /
213-
`liquidate` / `close_expired`.
214-
215-
### State / data accounts
216-
217-
- **`Lease`** - program-derived address with seeds `["lease", holder, lease_id]`. Data account owned by the program, holding all the lease parameters and current lifecycle state (see below).
218-
219-
### Token vaults
220-
221-
- **`leased_vault`** - program-derived address with seeds `["leased_vault", lease]`. Token account whose authority is itself (program-derived-address-signed). Holds `leased_amount` while `Listed`; `0` while `Active` (the short seller has the tokens); full amount again briefly inside `return_lease`.
222-
- **`collateral_vault`** - program-derived address with seeds `["collateral_vault", lease]`. Token account whose authority is itself (program-derived-address-signed). Holds `0` while `Listed`; `collateral_amount` while `Active`, decreasing as lease fee streams out and increasing on `top_up_collateral`.
223-
224-
### User accounts passed in
225-
226-
- **`holder` wallet** (user-owned) - `create_lease` signer, receives the lease fee and final recovery.
227-
- **`short_seller` wallet** (user-owned) - `take_lease` / `top_up_collateral` / `return_lease` signer.
228-
- **`keeper` wallet** (user-owned) - `liquidate` signer, receives the bounty.
229-
- **`payer` wallet** (user-owned) - `pay_lease_fee` signer (can be anyone, not just the short seller).
230-
- **`holder_leased_account`** - holder's [associated token account](https://solana.com/docs/terminology) for the leased mint; source on `create_lease`, destination on `return_lease` / `close_expired`.
231-
- **`holder_collateral_account`** - holder's associated token account for the collateral mint; destination for the lease fee and liquidation proceeds.
232-
- **`short_seller_leased_account`** - short seller's associated token account for the leased mint; destination on `take_lease`, source on `return_lease`.
233-
- **`short_seller_collateral_account`** - short seller's associated token account for the collateral mint; source on `take_lease` / `top_up_collateral`, destination for collateral refund on `return_lease`.
234-
- **`keeper_collateral_account`** - keeper's associated token account for the collateral mint; receives the liquidation bounty.
235-
- **`price_update`** - `PriceUpdateV2` account owned by the Pyth Receiver program, for the feed the lease is pinned to.
236-
237-
### Fields on `Lease`
238-
239-
From [`state/lease.rs`](programs/asset-leasing/src/state/lease.rs):
240-
241-
```rust
242-
pub struct Lease {
243-
pub lease_id: u64, // caller-supplied id so one holder can run many leases
244-
pub holder: Pubkey, // who listed it, gets paid the lease fee
245-
pub short_seller: Pubkey, // who took the lease; Pubkey::default() while Listed
246-
247-
pub leased_mint: Pubkey,
248-
pub leased_amount: u64, // locked at creation, unchanging
249-
250-
pub collateral_mint: Pubkey,
251-
pub collateral_amount: u64, // increases on top_up, decreases as lease fees pay out
252-
pub required_collateral_amount: u64, // what the short seller must post on take_lease
253-
254-
pub lease_fee_per_second: u64, // denominated in collateral units
255-
pub duration_seconds: i64,
256-
pub start_timestamp: i64, // 0 while Listed
257-
pub end_timestamp: i64, // 0 while Listed; start_timestamp + duration once Active
258-
pub last_paid_timestamp: i64, // Lease fee accrues from here to min(now, end_timestamp)
259-
260-
pub maintenance_margin_basis_points: u16, // e.g. 12_000 = 120%
261-
pub liquidation_bounty_basis_points: u16, // e.g. 500 = 5%
262-
263-
pub feed_id: [u8; 32], // Pyth feed_id this lease is pinned to
264-
265-
pub status: LeaseStatus, // Listed | Active | Liquidated | Closed
266-
267-
pub bump: u8,
268-
pub leased_vault_bump: u8,
269-
pub collateral_vault_bump: u8,
270-
}
271-
```
272-
273-
The `Closed` and `Liquidated` states are not directly observable
274-
onchain: all three of `return_lease`, `liquidate` and `close_expired`
275-
close the `Lease` account in the same transaction (`close = holder`),
276-
returning the rent-exempt lamports to the holder. The in-memory
277-
`status` field is set *before* the close so the transaction logs
278-
record the terminal state, but the account disappears at the end.
279-
280-
---
281-
282207
## Lifecycle
283208

284209
### What the short seller really gets
@@ -308,16 +233,34 @@ The holder calls `create_lease`, naming the leased mint, the
308233
collateral mint, the amount of leased tokens to offer, the
309234
collateral the short seller will have to post, the per-second lease
310235
fee, the duration, the maintenance-margin and liquidation-bounty
311-
ratios, and the Pyth `feed_id` the lease will be priced against. The
312-
program creates the `Lease` account, creates two empty token vault
313-
[program-derived addresses](https://solana.com/docs/terminology) (one for each
314-
mint), and moves the leased tokens out of the holder's wallet into
315-
the leased vault. Locking the leased tokens up front means a short
316-
seller calling `take_lease` later cannot fail because the holder
317-
spent the inventory in the meantime - the atomicity guarantee
236+
ratios, and the Pyth `feed_id` the lease will be priced against.
237+
This is where every account the rest of the lifecycle uses gets
238+
created. The handler initialises three
239+
[program-derived addresses](https://solana.com/docs/terminology):
240+
241+
- **`Lease`** - the state account, owned by the program, holding all
242+
the lease parameters and the current lifecycle status. Seeds:
243+
`[b"lease", holder, lease_id.to_le_bytes()]` - keying on
244+
`lease_id` lets one holder run many leases in parallel.
245+
- **`leased_vault`** - a token account for the leased mint whose
246+
authority is itself (the program signs as the vault using the
247+
vault's own seeds). Seeds: `[b"leased_vault", lease]`. Holds
248+
`leased_amount` while `Listed`; `0` while `Active` (the short
249+
seller has the tokens); the full amount again briefly inside
250+
`return_lease`.
251+
- **`collateral_vault`** - a token account for the collateral mint,
252+
also self-authoritative. Seeds: `[b"collateral_vault", lease]`.
253+
Created empty here; filled by `take_lease`, drained over time as
254+
lease fees stream out, and topped up by `top_up_collateral`.
255+
256+
The handler then moves the leased tokens out of the holder's wallet
257+
into the leased vault. Locking the leased tokens up front means a
258+
short seller calling `take_lease` later cannot fail because the
259+
holder spent the inventory in the meantime - the atomicity guarantee
318260
transfers to the program the moment the lease is listed.
319261

320-
- **Signers:** `holder`.
262+
- **Signers:** `holder` (the user wallet listing the tokens; receives
263+
the lease fee and the final recovery).
321264
- **Accounts:**
322265
- `holder` (signer, mut - pays account rent)
323266
- `leased_mint`, `collateral_mint` (read-only)
@@ -343,25 +286,65 @@ transfers to the program the moment the lease is listed.
343286
- `InvalidMaintenanceMargin` if `maintenance_margin_basis_points` is `0` or `> 50_000`
344287
- `InvalidLiquidationBounty` if `liquidation_bounty_basis_points > 2_000`
345288

289+
#### What's on the lease account
290+
291+
The `Lease` account written above carries the full set of fields
292+
referenced by the rest of the lifecycle. From [`state/lease.rs`](programs/asset-leasing/src/state/lease.rs):
293+
294+
```rust
295+
pub struct Lease {
296+
pub lease_id: u64, // caller-supplied id so one holder can run many leases
297+
pub holder: Pubkey, // who listed it, gets paid the lease fee
298+
pub short_seller: Pubkey, // who took the lease; Pubkey::default() while Listed
299+
300+
pub leased_mint: Pubkey,
301+
pub leased_amount: u64, // locked at creation, unchanging
302+
303+
pub collateral_mint: Pubkey,
304+
pub collateral_amount: u64, // increases on top_up, decreases as lease fees pay out
305+
pub required_collateral_amount: u64, // what the short seller must post on take_lease
306+
307+
pub lease_fee_per_second: u64, // denominated in collateral units
308+
pub duration_seconds: i64,
309+
pub start_timestamp: i64, // 0 while Listed
310+
pub end_timestamp: i64, // 0 while Listed; start_timestamp + duration once Active
311+
pub last_paid_timestamp: i64, // Lease fee accrues from here to min(now, end_timestamp)
312+
313+
pub maintenance_margin_basis_points: u16, // e.g. 12_000 = 120%
314+
pub liquidation_bounty_basis_points: u16, // e.g. 500 = 5%
315+
316+
pub feed_id: [u8; 32], // Pyth feed_id this lease is pinned to
317+
318+
pub status: LeaseStatus, // Listed | Active | Liquidated | Closed
319+
320+
pub bump: u8,
321+
pub leased_vault_bump: u8,
322+
pub collateral_vault_bump: u8,
323+
}
324+
```
325+
346326
### The short seller takes the offer - `take_lease`
347327

348328
A short seller who has spotted the `Lease` account onchain (via an
349329
indexer or a direct lookup) calls `take_lease` to take delivery. The
350-
program deposits the short seller's collateral first, then hands over
351-
the leased tokens - depositing collateral first means that if the
352-
leased-token payout fails for any reason the whole transaction
353-
reverts and the short seller gets their collateral back. The lease
354-
moves from `Listed` to `Active`.
355-
356-
- **Signers:** `short_seller`.
330+
program deposits the short seller's collateral into `collateral_vault`
331+
first - the vault was created empty by `create_lease` and this is
332+
the call that fills it - then hands over the leased tokens.
333+
Depositing collateral first means that if the leased-token payout
334+
fails for any reason the whole transaction reverts and the short
335+
seller gets their collateral back. The lease moves from `Listed` to
336+
`Active`.
337+
338+
- **Signers:** `short_seller` (the user wallet borrowing the tokens
339+
and posting collateral).
357340
- **Accounts:**
358341
- `short_seller` (signer, mut)
359342
- `holder` (UncheckedAccount - read for program-derived address seed derivation only, no signature required)
360343
- `lease` (mut, `has_one = holder`, `has_one = leased_mint`, `has_one = collateral_mint`, must be `Listed`)
361344
- `leased_mint`, `collateral_mint`
362345
- `leased_vault`, `collateral_vault` (both mut, both program-derived addresses)
363-
- `short_seller_collateral_account` (mut, short seller's associated token account - source)
364-
- `short_seller_leased_account` (mut, **init_if_needed** - destination)
346+
- `short_seller_collateral_account` (mut, short seller's associated token account for the collateral mint - source)
347+
- `short_seller_leased_account` (mut, **init_if_needed** - short seller's associated token account for the leased mint, destination)
365348
- `token_program`, `associated_token_program`, `system_program`
366349
- **What happens:**
367350
- Two token movements, in order:
@@ -395,13 +378,14 @@ the vault. Fees do not accrue past `end_timestamp` - once the
395378
deadline hits, the short seller is either returning the tokens,
396379
being liquidated, or defaulting; no further lease fees are owed.
397380

398-
- **Signers:** `payer` (anyone).
381+
- **Signers:** `payer` (any user wallet - the short seller, a
382+
keeper bot, or anyone else willing to pay the transaction fee).
399383
- **Accounts:**
400384
- `payer` (signer, mut - pays for `init_if_needed` of the holder associated token account)
401385
- `holder` (UncheckedAccount, read-only - used for `has_one` check)
402386
- `lease` (mut, must be `Active`)
403387
- `collateral_mint`, `collateral_vault`
404-
- `holder_collateral_account` (mut, **init_if_needed**)
388+
- `holder_collateral_account` (mut, **init_if_needed** - holder's [associated token account](https://solana.com/docs/terminology) for the collateral mint, destination for the lease fee)
405389
- `token_program`, `associated_token_program`, `system_program`
406390
- **What happens:**
407391
- Compute `lease_fee_due = (min(now, end_timestamp) - last_paid_timestamp) * lease_fee_per_second`.
@@ -501,6 +485,15 @@ time at `end_timestamp`.
501485
- `Unauthorised` if `lease.short_seller != short_seller.key()`
502486
- `MathOverflow` if the lease-fee or collateral subtraction overflows
503487

488+
`return_lease` is the first place an account-close happens; the same
489+
mechanism runs in `liquidate` and `close_expired`. The `Closed` and
490+
`Liquidated` states are not directly observable onchain: all three
491+
of `return_lease`, `liquidate` and `close_expired` close the `Lease`
492+
account in the same transaction (`close = holder`), returning the
493+
rent-exempt lamports to the holder. The in-memory `status` field is
494+
set *before* the close so the transaction logs record the terminal
495+
state, but the account disappears at the end.
496+
504497
### Branch: position underwater - `liquidate`
505498

506499
If the leased asset rallies far enough that the locked collateral is
@@ -523,16 +516,18 @@ where `debt_value = leased_amount * price * 10^exponent`, with the
523516
Pyth exponent folded into whichever side of the inequality keeps the
524517
math non-negative (see [`is_underwater`](programs/asset-leasing/src/instructions/liquidate.rs)).
525518

526-
- **Signers:** `keeper`.
519+
- **Signers:** `keeper` (any user wallet - typically a bot watching
520+
for underwater positions; receives the bounty as payment for
521+
cleaning up).
527522
- **Accounts:**
528523
- `keeper` (signer, mut - pays `init_if_needed` cost for both associated token accounts)
529524
- `holder` (UncheckedAccount, mut - receives lease fee, holder share, and the rent-exempt lamports from the three closed accounts)
530525
- `lease` (mut, `close = holder`, must be `Active`)
531526
- `leased_mint`, `collateral_mint`
532527
- `leased_vault`, `collateral_vault` (both mut)
533528
- `holder_collateral_account` (mut, **init_if_needed**)
534-
- `keeper_collateral_account` (mut, **init_if_needed**)
535-
- `price_update` (UncheckedAccount, constrained to `owner = PYTH_RECEIVER_PROGRAM_ID`)
529+
- `keeper_collateral_account` (mut, **init_if_needed** - keeper's [associated token account](https://solana.com/docs/terminology) for the collateral mint, destination for the bounty)
530+
- `price_update` (UncheckedAccount, constrained to `owner = PYTH_RECEIVER_PROGRAM_ID`) - a `PriceUpdateV2` account owned by the Pyth Receiver program for the feed the lease was pinned to at creation. This is the first handler that requires the oracle account itself; `create_lease` only stores the `feed_id` it expects to see here.
536531
- `token_program`, `associated_token_program`, `system_program`
537532
- **What happens:**
538533
- Decode `price_update`: discriminator must match

0 commit comments

Comments
 (0)