分share math
kagura-vault uses standard erc-4626-style share math: shares represent fractional ownership of the principal token account. accrual changes the balance, not the share count. price reflects yield as a side effect.
the three numbers
Vault accounting reduces to three observable quantities:
| name | where | meaning |
|---|---|---|
vault_balance | principal_account.amount | total usdc currently held by the vault. base assets. |
total_shares | share_mint.supply | total minted vault shares. integer count. |
share_value | derived | vault_balance / total_shares. usdc per share. |
the three formulas
1pub fn calc_accrued_funding(principal: u64, rate_bps: u32, elapsed_ms: u64) -> Result<u64> {2 if principal == 0 || rate_bps == 0 || elapsed_ms == 0 { return Ok(0); }3 let n = (principal as u128)4 .checked_mul(rate_bps as u128)?5 .checked_mul(elapsed_ms as u128)?;6 let d = 10_000u128.checked_mul(31_536_000_000)?; // ms per year7 Ok((n.checked_div(d)? as u64))8}9 10pub fn calc_shares_to_mint(amount: u64, total_shares: u64, vault_balance: u64) -> Result<u64> {11 if total_shares == 0 || vault_balance == 0 { return Ok(amount); }12 Ok(((amount as u128).checked_mul(total_shares as u128)?13 .checked_div(vault_balance as u128)? as u64))14}15 16pub fn calc_assets_to_redeem(shares: u64, total_shares: u64, vault_balance: u64) -> Result<u64> {17 if total_shares == 0 { return Err(VaultError::InsufficientShares.into()); }18 Ok(((shares as u128).checked_mul(vault_balance as u128)?19 .checked_div(total_shares as u128)? as u64))20}deposit
First deposit mints 1:1 shares (no prior holders to dilute):
1amount = 1_000 USDC = 1_000 × 10^6 = 1,000,000,000 base units2shares = amount = 1,000,000,000Subsequent deposits mint shares proportional to the existing share-to-balance ratio:
1vault_balance_before = 1,002,500 (yield accrued since first deposit)2total_shares_before = 1,000,0003amount = 500,0004 5shares_to_mint = amount × total_shares / vault_balance6 = 500,000 × 1,000,000 / 1,002,5007 ≈ 498,7548 9(slightly fewer shares per unit assets, because each share is now worth slightly more)tick
On tick_funding the vault increases vault_balance by the accrued amount (a token transfer from the treasury reserve). It does not mint or burn shares. Therefore share_value rises proportionally.
1elapsed_ms = 15002principal = 10,000_000_000 (10,000 USDC in micro-USDC)3rate_bps = 2200 (22% APR target)4 5accrued = 10_000_000_000 × 2200 × 1500 / (10_000 × 31_536_000_000)6 = 33_000_000_000_000_000 / 315_360_000_000_0007 ≈ 104 (micro-USDC per tick)At $10,000 deposit, 22% APR target, 1.5s ticks, you earn ≈ 104 micro-USDC per tick = ≈ $0.0001 per tick = ≈ $6 per day = ≈ $2,200 per year. The math round-trips.
withdraw
1shares_burned = 500,000 (half of the holder's balance, say)2total_shares_before = 1,000,0003vault_balance_before = 1,200,000 (after some hours of accrual)4 5assets_out = shares × vault_balance / total_shares6 = 500,000 × 1,200,000 / 1,000,0007 = 600,0008 9(the depositor receives 600k of base USDC, having put in ~500k earlier)overflow safety
All intermediate products are computed in u128. Final results check whether they fit in u64 and error with MathOverflow if not. The unit tests in programs/kagura-vault/src/math.rs cover:
- 22% APR, 1h elapsed, 1k USDC principal — within 200 micro-USDC of expected.
- 22% APR, 1s elapsed, 10M USDC principal — within 100 micro-USDC of expected.
- 22% APR, 400ms elapsed, 1k USDC principal — truncates to zero (dust below scale).
- first deposit shares = amount.
- subsequent deposit shares dilute proportionally to yield.
- redeem returns assets proportional to share fraction.