APR, APY, and Time-Based Interest Accrual
From simple and compound interest to a call-independent index that accrues yield without letting user activity choose the compounding schedule.
View the tested implementationDay 2 ended with a rule for finite arithmetic: rounding is part of economic policy. Day 3 adds time to that policy.
A rate is incomplete until we know the period it describes, how it compounds, and what state it applies to.
That sounds like ordinary interest-rate arithmetic. The harder problem appears when the equations become shared protocol state. A vault must accrue yield for existing users, price later users fairly, survive integer rounding, and produce the same economics regardless of who happens to call it.
The complete Rust implementation and its 119 passing tests live in the DeFi accounting repository.
Principal, interest, and return
Suppose Alice provides 2,000 USDC and receives 2,300 USDC one year later. The original 2,000 is the principal. The additional 300 is interest.
Dividing by the final 2,300 would answer a different question: what fraction of the final balance is profit? Investment return measures growth relative to the capital that produced it, so the denominator is the original principal.
For simple interest, let P be principal, r the annual rate, and t time measured in years:
At 15% per year, 2,000 USDC earns 150 over six months—not 300—because six months is half a year:
The time unit is part of the calculation. A 15% return earned in six months annualises to 30% under a simple extrapolation; it is not the same claim as 15% per year.
Compounding changes the base
Simple interest always refers back to the original principal. Compound interest adds each period's interest to the balance used by the next period.
At 10% annually, 1,000 becomes 1,100 after one year. During year two, interest is calculated on 1,100:
The final balance is 1,210 rather than the 1,200 produced by two years of simple interest. The extra 10 is interest earned on earlier interest.
With n compounding periods per year over t years:
The rate per period is r / n; the number of periods is nt. At 12% compounded monthly, the monthly rate is 1% and the balance is multiplied by 1.01 twelve times.
APR and APY answer different questions
APR is the nominal annual rate before within-year compounding. APY is the effective one-year return after the stated compounding schedule:
For 12% APR compounded monthly:
For 24% APR compounded monthly:
Two products can display the same APR and produce different one-year returns if they compound at different frequencies. Likewise, two products can both display “20%” while one means APR and the other means APY. The labels cannot be compared without first normalising the convention.
More frequent compounding increases APY for a positive rate because earned interest joins the productive balance earlier. The incremental benefit diminishes as the periods become shorter.
Annualising is an extrapolation
Suppose 1,000 grows to 1,050 over three months. The observed return is 5%.
A simple annualisation multiplies it by the four three-month periods in a year:
Compounded annualisation assumes that the same result repeats and is reinvested:
Neither number is a return the vault has already delivered. Both project a short observation across an entire year. That distinction matters in DeFi because utilisation, trading volume, incentives, token prices, losses, and strategy allocation can all change. Aave's official disclosures make the same practical point for displayed vault rates: they are indicative, variable, and not guaranteed (Aave App disclosures).
A three-month path of +2%, +1%, and −0.5% demonstrates why projecting the first period alone is unreliable:
The realised three-month return is 2.5049%, not the rate implied by pretending that the first month's 2% persists forever.
Yield belongs in the share price
For a vault with assets A and shares S:
If Alice deposits 10,000 assets into an empty vault and receives 10,000 shares, the initial price is 1. When the strategy earns yield, assets increase while shares remain unchanged. At 6% growth:
Bob can then deposit 1,060 assets and receive 1,000 shares. He pays the current price of 1.06, so he receives none of the return earned before he entered. This is the same proportional-ownership model defined for tokenized vaults by ERC-4626.
The ordering is therefore not optional:
Pricing Bob at the stale price of 1 would mint too many shares and transfer part of Alice's historical yield to him.
Programs do not wake themselves up
A Solana program runs only when a transaction invokes it. It cannot continuously update an account in the background. A deployed version of this model would read the cluster's current time from the Clock sysvar when an instruction executes; Solana documents sysvars as read-only accounts that expose cluster state, including the current slot, epoch, and Unix timestamp (Solana account types).
The model uses lazy accrual:
- Read the current time.
- Compare it with the last observed time.
- Calculate the growth implied by the elapsed interval.
- Apply only the growth not already reflected in state.
- Save the new observation atomically.
A repeated timestamp is a no-op. A backwards timestamp is rejected. If unsigned subtraction interpreted 900 − 1,000 as an enormous elapsed interval, it could produce absurd interest or overflow rather than a sensible negative duration.
Fixed-point rates
Financial state transitions should not depend on binary floating-point approximations. The Rust model represents rates with an integer scale:
pub const RATE_SCALE: u128 = 1_000_000_000;
pub const SECONDS_PER_YEAR: u64 = 31_536_000;At that scale:
Simple time-prorated interest becomes:
The implementation widens intermediate multiplication to u128 and uses checked arithmetic. Rust's integer API returns None when operations such as checked_mul overflow, allowing the vault to reject the transition instead of wrapping silently (Rust u128 documentation).
Compound growth uses exponentiation by squaring. Its work grows logarithmically with the number of periods rather than looping once for every second or slot.
Remainders must survive repeated accrual
With 1,000 indivisible units at 9% APR, one month's simple interest is 7.5 units. Crediting 7 and discarding 0.5 every month produces only 84 units over a year, even though the annual calculation produces 90.
The implementation avoids that repeated loss by recomputing cumulative entitlement from a stable index and applying only the difference already unaccounted for. A fraction that cannot yet become one base unit remains represented in the higher-precision state until enough time passes to materialise it.
The vault never transfers a fractional token; it preserves fractional accounting until it becomes a complete base unit.
The first design passed its tests and was still wrong
The initial Day 3 implementation used a checkpoint principal. Repeated calls to accrue were safe: calculating once at the end or several times along the way produced the same result.
But every deposit, mint, withdrawal, and redemption moved the checkpoint to the current balance. That balance already contained accrued interest. A user could therefore choose the effective compounding schedule by making tiny deposits.
At an exaggerated 100% linear APR:
- 1,000 left untouched for one year correctly becomes 2,000.
- Accruing after six months produces 1,500.
- A minimal deposit resets the checkpoint around that balance.
- The next six months then accrue against approximately 1,500.
- The original capital approaches 2,250—a 125% return created by transaction timing.
The direct accrual-frequency property passed. The broader economic invariant did not.
Replacing checkpoints with an index
The corrected model separates position size from position value:
scaled_principalrecords pool value normalised at the index applicable when capital entered.interest_indexrecords cumulative growth.- Deposits and withdrawals change scaled principal.
- Time and rate changes move the index.
- User operations never choose when historical interest becomes new principal.
Conceptually:
The index is derived from an epoch start index, epoch start time, and the active rate. Its anchor moves on a rate change—not whenever a user touches the vault. Before installing a new rate, the vault settles the old rate up to the change timestamp so the new rate cannot apply retroactively.
This also keeps profit and loss connected to the same accounting representation. Timestamp-aware apply_profit_at and apply_loss_at operations settle the index before booking an external strategy result.
Correcting the Alice and Bob example
The original worked example accidentally compounded the 12% linear rate at the six-month deposit boundary. It ended at a share price of 1.1236, equivalent to multiplying by 1.06 twice.
Under the corrected linear index, Alice's capital follows the full-year index from 1 to 1.12. Bob enters when the index is 1.06 and buys 1,000 shares for 1,060 assets. At year end:
Alice's 10,000 shares claim 11,200 assets, giving her the declared 12% full-year linear return:
Bob's 1,000 shares claim 1,120 assets:
Bob's return is 1.12 / 1.06 − 1, approximately 5.66%. A shared linear index is linear in absolute time; it is not a fresh six-month simple-interest contract opened independently for every depositor.
The claims reconcile exactly:
Most importantly, inserting additional tiny deposits cannot increase Alice's return beyond 1,200. Her outcome is governed by the index, not somebody else's transaction frequency.
Atomicity and adversarial cases
Every combined operation computes on a prospective state and commits only after accrual and the user action both succeed. This mirrors the all-or-nothing property of Solana transactions: when an instruction fails, state changes from the transaction are rolled back (Solana core concepts).
The tests cover more than the happy-path equations:
- Repeated accrual at one timestamp is idempotent.
- Splitting accrual calls produces the same final state.
- Tiny deposits cannot compound pre-existing capital.
- Later deposits participate only through their entry index.
- Withdrawn capital stops participating after exit.
- Rate changes do not apply retroactively.
- Backwards timestamps and arithmetic overflow fail atomically.
- Profit and loss remain synchronized with subsequent accrual.
- User balances still sum to total shares.
- Aggregate user claims remain bounded by vault assets.
The repository now contains 119 tests: 62 unit tests, 16 interest property tests, 7 interest scenarios, 10 vault property tests, and 24 vault scenarios.
What I learned
APR and APY are the easy part. The harder lesson was deciding what controls the passage from a quoted rate to protocol state.
- APR excludes within-year compounding; APY includes a specified compounding convention.
- Annualising a short observation projects a result; it does not guarantee it.
- Yield increases assets and share price, not share supply.
- Existing yield must be accrued before a new user is priced.
- Fixed-point remainders should accumulate rather than disappear at every call.
- A rate change must settle the old interval before becoming active.
- Calling a method more often must not manufacture yield.
- Testing an implementation invariant is insufficient when the underlying economic invariant is incomplete.
The final point changed the implementation. The first design proved exactly what its tests asked it to prove—and still allowed user activity to select a compounding schedule. The fix came from strengthening the question.
Day 4 moves from earning yield to charging for its management: management fees, performance fees, and high-water marks.
References
- Ethereum Improvement Proposals, ERC-4626: Tokenized Vaults.
- Solana, Core concepts and atomic transactions.
- Solana, Account types and the Clock sysvar.
- Rust standard library,
u64andu128. - Aave, Aave App disclosures: rates and yield risk.