How it works
This page follows one position through its life and shows the exact rule the program applies at each step. Code references point at programs/uruoi/src.
Values are in SOL, read from the pool
Every collateral has a rate source, set once when the collateral is added:
| Collateral kind | Rate source account | Rate the program reads |
|---|---|---|
SplStakePool (JitoSOL) | the SPL stake pool account | total_lamports / pool_token_supply |
Marinade (mSOL) | the Marinade state account | msol_price, lamports per mSOL in 32.32 fixed point |
The program keeps rates as Q64.64 fixed point numbers (math.rs, Rate). The SOL value of a position is shares x rate, rounded down. The rate source is pinned to the collateral account, and the program checks that the source belongs to the right program, has the expected layout, and prices the right mint (InvalidRateSource, RateSourceMintMismatch).
Borrowing, withdrawing while a debt is open, repaying with collateral, redeeming and unwinding need a fresh rate: an SPL stake pool that has not been updated for the current epoch is refused with StaleRate, so nobody can borrow against a rate the pool has not confirmed yet. Settling (and so sync) works on any rate; a stale one simply sweeps what it shows.
Settle: the sweep runs first, every time
Every position instruction starts with settle (instructions/position.rs). It reads today's pool rate and applies the sweep:
// math.rs
pub fn sweep(shares: u64, debt: u64, snapshot: Rate, now: Rate) -> Option<Sweep> {
if debt == 0 || shares == 0 || now <= snapshot {
return Some(Sweep::default());
}
let earned = u128::from(shares).checked_mul(now.0 - snapshot.0)? >> 64;
let swept = u64::try_from(earned.min(u128::from(debt))).ok()?;
if swept == 0 {
return Some(Sweep::default());
}
let shares_out = now.shares_ceil(swept)?.min(shares);
Some(Sweep { swept, shares_out })
}
In words:
earnedis what your shares gained since the last snapshot: shares times the rate rise, in lamports.sweptis that gain, capped at your debt. Once the debt is gone, the gain stays with you.shares_outis how many LST shares are worthsweptlamports at today's rate, rounded up, so the buffer always holds at least the SOL value credited to your debt.- Your debt falls by
swept, your shares fall byshares_out, and those shares move to the collateral's redemption buffer (buffer_shares). - Your snapshot moves to today's rate. A falling rate sweeps nothing.
Because the swept shares are worth exactly the gain, your position keeps the SOL value it started with, and the debt goes down by the yield on that value.
Sync: anyone can push it along
sync is permissionless. It runs settle on any position and nothing else. Anyone may call it, for any position, at any time; a keeper that syncs every position once an epoch keeps every debt current. You never need to sync your own position: any instruction you send settles first.
If nobody syncs for a while, nothing is lost. The next settle uses the shares and the snapshot from the last one, so the gain covers the whole stretch at once.
Borrow
borrow(amount) settles, needs a fresh rate, then checks:
- borrowing is not paused for this collateral (
MintingPaused), (debt + amount) / value <= max_ltv(ExceedsMaxLtv), computed asdebt x 10,000 <= value x max_ltv_bps,- the collateral's total debt stays under its debt ceiling (
DebtCeilingReached).
Then it mints amount uSOL to your token account and emits Borrowed.
Repay
There are two ways to pay early:
repay(amount): anyone burns up toamountuSOL against any position's debt. It can only lower a debt. Anything above the remaining debt stays in the payer's account.repay_with_collateral(amount): the owner pays with the position's own LST at the pool rate. The LST moves to the redemption buffer, where it backs the uSOL this debt once created.
Withdraw and close
withdraw(amount) settles, then, if the position still has debt, needs a fresh rate and releases LST only if the remaining shares keep the debt within the borrow limit. With no debt, it releases any amount up to the shares held. close_position closes an empty position (no shares, no debt) and returns its rent.
Redeem
redeem(amount) burns amount uSOL and pays exactly amount lamports of LST, taken from every collateral's buffer in proportion to that buffer's SOL value (math::pro_rata). Every redeemer receives the same mix, so nobody can pick the best LST and leave a weaker one behind for the last holders. If the buffers together hold less than amount, the call fails with InsufficientBuffer.
Unwind
unwind(amount) is the only forced path. It opens only when a position's loan to value is above its collateral's unwind threshold, which can only happen if the pool rate itself has fallen. The unwinder burns uSOL against the debt and takes the same SOL value of the position's LST at the pool rate, never more than it takes to bring the position back down to the borrow limit (math::unwind_limit). A healthy position refuses with PositionHealthy.
Diagram, in MermaidsequenceDiagram
participant Owner
participant Program
participant Pool as Stake pool
participant Buffer as Redemption buffer
Owner->>Program: deposit(LST)
Owner->>Program: borrow(uSOL)
Program->>Owner: mint uSOL
loop every epoch
Pool-->>Pool: rate rises
Note over Program: anyone calls sync
Program->>Buffer: shares worth the rise
Program->>Program: debt -= rise
end
Owner->>Program: withdraw(all LST) at zero debt