Divine Igbinoba / Case Study / Turbine Web3 Capstone

Turbine Web3 · Q1 2026 Capstone

Atlas
Payroll.

Streaming Payroll with Operational Capital Yield on Solana

Type
On-chain DeFi Protocol
Chain
Solana (mainnet-beta)
Yield Source
Kamino Finance USDC Market
Cohort
Turbine Web3 2026 Cohort 1
Rust Anchor Solana SVM Kamino Finance CPIs TypeScript bytemuck Surfpool zero_copy USDC PDA CPI

The Problem

Payroll’s broken: Workers wait.
Cash sits idle.

Traditional payroll has a structural mismatch. Employees accrue salary continuously, but payment arrives once a month. Meanwhile, expenses don't wait for payday.

For the employer, idle payroll capital earns almost nothing. Banks offer 0.5–2% APY on business accounts. Large institutions have the infrastructure to capture yield on idle capital. Most startups don't. The money just sits there.

Atlas Payroll was built to fix both problems simultaneously.

48hrs
Half of every paycheck is spent within 48 hours of receipt — but pay arrives once a month. Expenses do not.
Source: Talker Research
$275
The average annual cost of the pay gap in overdraft fees and short-term loan interest — a hidden tax on workers who can least afford it.
Source: StudyFinds
0.5–2%
APY on business bank accounts — where most startups park payroll capital while waiting for the next payment cycle.
vs. 13.35% APY on Kamino Finance USDC (DefiLlama)
Infrastructure gap between large institutions that already capture yield on idle capital, and startups that have no safe way to do the same.
Palus · Slash · Ramp · PR Newswire

The Solution

Stream salary per second.
Yield on everything else.

Atlas routes idle payroll capital into Kamino Finance, a Solana-native lending protocol, earning yield on funds between disbursement cycles.

Simultaneously, employees accrue salary every second and can withdraw earned income at any point, with no loans, no overdrafts, no waiting.

If you earn $5,000 per month and need cash on March 13th, you can withdraw what you've earned so far, approximately $2,094 rather than taking a payday loan. Meanwhile, the remaining $2,906 is compounding at 13.35% APY inside Kamino instead of sitting idle in a bank account.

Operator
Deposits payroll capital. Manages employee accounts. Subject to liability constraints on all withdrawals.
Employee
Salary accrues every second. Claimable anytime against the safety vault or Kamino directly.
Kamino Finance
External lending protocol earning yield on idle USDC. kUSDC tokens represent the yield-bearing position.
Safety Vault
Holds a 2.2-day payroll buffer in liquid USDC at all times. First source for all employee claims.
Keeper
Off-chain agent that earns a $1 USDC bounty for rebalancing funds from Kamino back to the Safety Vault.

System Architecture

System Flow.

The protocol's financial logic splits into three independent sub-systems: operator capital management (deposit, withdraw), employee salary mechanics (accrue, claim, offboard), and the rebalance loop (Kamino → Safety Vault). Each is an isolated instruction set, they share state through the ProtocolVault account but cannot corrupt each other's invariants.

1 — Capital Provisioning (Deposit & Stake)

Operator DepositIx ProtocolVault Protocol_ATA Kamino execute deposit(amount) update_liability() Transfer USDC from Operator CPI: deposit_reserve_liquidity(USDC) Mint kTokens (collateral) yield_amount += minted_kTokens Operator DepositIx ProtocolVault Protocol_ATA Kamino

Deposit flow — liability checkpointed first, USDC routed to Kamino via CPI, kTokens minted to Protocol_ATA and recorded on ProtocolVault

2 — Keeper Rebalance Circuit (Yield Extraction & Liquidity Buffering)

Keeper RebalanceIx ProtocolVault Kamino Protocol_ATA execute rebalance() calculate target buffer (48h global_rate) alt [Target > safety_amount] CPI: redeem_reserve_collateral(delta_ktokens) Release USDC Equivalent Transfer platform fee → Platform Transfer bounty → Keeper Update safety_amount (+USDC) Keeper RebalanceIx ProtocolVault Kamino Protocol_ATA

Rebalance flow — Keeper triggers only when target > safety_amount; redeems kTokens via CPI, distributes bounty and fee, updates the vault buffer

3 — Employee Claim Flow (JIT Withdrawal)

Staff ClaimIx StaffAccount ProtocolVault Kamino execute claim() global update_liability() calc claimable ((rate × time) − claimed) alt [claimable > safety_amount] calculate required JIT Liquidity CPI: redeem_reserve_collateral(missing_kTokens) Extract Liquid USDC Transfer USDC → Staff_ATA liability -= claim_amount total_claimed += claim_amount Staff ClaimIx StaffAccount ProtocolVault Kamino

Claim flow — liability checkpointed globally, claimable computed per employee; JIT Kamino redemption fires only when safety_amount is insufficient

What It Delivers

Shipped

1/sec
Salary accrual granularity — per second, not per month
31,557,600 seconds per year · Gregorian calendar
13.35%
APY on idle payroll capital via Kamino Finance USDC market
vs. 0.5–2% on business bank accounts
100%
Employee claim uptime via vault-first, Kamino fallback architecture
Autonomous fallback — no manual intervention
2.2d
Safety vault buffer — 10% over 2-day minimum for operational margin
Accounts for network latency and keeper scheduling

Technical Decisions

Key Decisions and Solutions

Each decision below solved a constraint that didn't have an obvious answer.

01 The liability timestamp — why you must checkpoint before mutating state +

Protocol Design · Salary Accrual · On-chain Accounting

Employee salary accrues continuously. The protocol tracks it through two fields on ProtocolVault: global_rate (the sum of all active per-second salary rates) and liability_timestamp (the last time the liability was checkpointed). Current total liability is derived on demand:

liability = recorded_liability + (global_rate × (now − liability_timestamp))
// checkpoint: update liability_timestamp = now

The critical constraint: always call update_liability() before mutating global_rate — specifically, adding or removing an employee.

Suppose the global rate is 10 USDC/sec and you add an employee who earns 2 USDC/sec. If you update the rate to 12 before checkpointing, then the next liability calculation will use 12 USDC/sec all the way back to liability_timestamp — retroactively overcounting what the protocol owes by 2 USDC/sec × elapsed time. The employee didn't start earning until now, but the arithmetic acts like they were earning since the last checkpoint.

The fix is simple, checkpoint first, mutate second. Every instruction that touches global_rate calls update_liability() as its very first line.

Getting this wrong doesn't crash the contract — it silently inflates or deflates what the protocol owes employees. On a financial protocol, silent arithmetic errors are the worst failure mode.
02 #[zero_copy] + bytemuck — reading Kamino's 3.5 KB Reserve without blowing the heap +

Solana SVM · Compute Units · bytemuck · Zero-Copy Deserialisation

To calculate how much USDC the protocol's kUSDC holdings are worth, we need to read Kamino's Reserve account — a large, complex struct that contains the full state of the lending market. It holds liquidity totals, collateral supply figures, interest rate curves, and a dozen other fields.

The standard Anchor approach — #[account] deserialisation — copies the entire struct from on-chain bytes into Solana's heap. For a 3.5+ KB struct, this is expensive in compute units and risks breaching Solana's heap limit, Especially in instructions that already perform multiple CPIs and token transfers.

#[zero_copy] with bytemuck avoids the copy entirely. Instead of deserialising the account into a Rust struct on the heap, it reinterprets the raw byte slice as a typed reference in-place. No allocation, no copy — the data is accessed directly from the account's memory as a Ref<Reserve>. Compute units for struct access drop to near zero.

The constraint bytemuck demands every field in the struct must have a known, fixed size with guaranteed alignment. This is where the u128 problem surfaces (see decision 3).

With Solana’s 4 KB stack limit and finite heap budge, Zero-copy is what makes reading a 3.5 KB external account feasible within Solana's compute model.
03 The u128 alignment split — and why Kamino's WAD is not 10¹⁸ +

Fixed-Point Arithmetic · bytemuck · Kamino Finance internals

Two separate problems here that are easy to conflate — both require precise understanding of how Kamino stores its numbers.

The u128 alignment problem. Kamino's Reserve struct stores several liquidity fields as u128 — 16-byte unsigned integers. bytemuck requires that all fields in a zero-copy struct have alignment guarantees matching their size. u128 requires 16-byte alignment. Solana account data begins with an 8-byte discriminator, meaning subsequent fields are aligned on 8-byte boundaries, not 16. Using u128 directly in the bytemuck struct causes runtime panic.

The fix: split each u128 field into two u64 values in the struct definition, then reconstruct the full u128 at runtime when the value is needed. Both halves are 8-byte aligned. The reconstruction is one arithmetic operation.

The WAD problem. Kamino stores its liquidity values in a u68.60 fixed-point format, not the common 1e18 WAD used in most DeFi protocols. The scale factor is 2^60 = 1,152,921,504,606,846,976. Using the wrong scale factor — say, treating a value as if it were scaled by 10^18 — produces exchange rate calculations that are off by roughly 10%.

// WRONG — common assumption, produces 10% error
total_pool_usdc = raw_value / 10^18

// CORRECT — Kamino's actual scale factor
total_pool_usdc = raw_value / 2^60 // 1,152,921,504,606,846,976
This is the kind of detail that doesn't appear in Kamino's public documentation — it required reading the source and cross-referencing DeepWiki's protocol analysis. Getting it wrong would have silently miscalculated every yield withdrawal in the system.
04 Ceiling division in ktoken_to_burn — why rounding direction matters +

Integer Arithmetic · Token Redemption · Solana SVM

When an employee claims salary or the keeper triggers a rebalance, the protocol needs to burn kUSDC tokens from Kamino to receive USDC in return. The calculation converts a required USDC amount into the equivalent kUSDC to burn:

ktoken_to_burn = required_usdc × total_ktoken / total_pool_usdc

The question is: which direction do we round the result?

Solana's SVM uses integer arithmetic exclusively. Division truncates toward zero by default. If we use floor division and the result has a fractional component, we burn slightly fewer kUSDC than required and Kamino returns slightly less USDC than we asked for. The employee's claim comes up short. In a protocol where every USDC unit matters, this is a silent underpayment.

Ceiling division guarantees the protocol always burns at least enough kUSDC to receive the required USDC. The employee is never shorted. The cost is that the protocol occasionally burns one extra unit of kUSDC — a negligible overpayment measured in fractions of a cent — but the employee always receives exactly what they're owed.

// Ceiling division in Rust (no standard library function)
ktoken_to_burn = (required_usdc × total_ktoken + total_pool_usdc - 1) / total_pool_usdc
The rounding direction decision has asymmetric consequences: floor division can underpay employees (bad); ceiling division can slightly overpay from reserves (acceptable). The correct choice is not about precision — it's about which party bears the rounding cost.
05 The Keeper incentive — making rebalancing economically self-sustaining +

Mechanism Design · Off-chain Agents · Protocol Economics

The safety vault needs to be replenished after employee claims draw it down. Something has to trigger the rebalance instruction. The simplest approach is a centralised bot controlled by Stellus — but that reintroduces a centralised dependency and single point of failure into a protocol designed to be permissionless.

The alternative is a permissionless Keeper: any wallet can call the rebalance instruction, and any successfull wallet earns a fixed $1 USDC bounty per call, plus the protocol levies a 0.5% tax on the total rebalanced amount for its treasury.

Two guards protect the system from keeper abuse. The first fires before any Kamino interaction: if the vault is already adequately funded, or if the available yield is smaller than the combined bounty + tax, the instruction returns silently with no state change. A keeper calling at the wrong time wastes only their own transaction fee. The second guard fires after the redemption: if the actual USDC received from Kamino falls below the cost threshold (due to rounding), the instruction also returns silently.

The economic logic: keepers are incentivised to watch for underfunded vaults and call rebalance quickly — they earn $1 per call. The protocol is incentivised to set the bounty high enough to attract keepers but low enough to not erode yield. Neither party has an incentive to behave maliciously, because the guards ensure any abusive call costs the caller a transaction fee and returns nothing.

This is mechanism design, not just code. The Keeper pattern eliminates the need for Stellus to run any infrastructure to maintain the protocol — the system is self-sustaining through economic incentives alone.

Testing Strategy

Testing against the real Kamino market

The test suite uses Surfpool to clone mainnet state into a local validator — every test runs against the exact on-chain data that production users would hit.

01
Discover the USDC Kamino Reserve
Query mainnet via getProgramAccounts with two memcmp filters — one matching the lending market address at byte offset 32, one matching the USDC mint at byte offset 128 — to locate the exact Reserve account without hardcoding its address.
02
Clone the reserve into Surfpool localnet
Surfpool clones the live Reserve account into the local validator, giving the test suite exact mainnet state including real liquidity ratios, real kUSDC supply, and real interest rate curves.
03
Zero out the stale slot counter
Kamino's deposit instruction checks that the Reserve was refreshed within the current slot. Since the cloned reserve reflects a past mainnet state, its last-updated slot is in the past. The slot counter at byte offset 16 is zeroed before tests run, bypassing this freshness check.
04
Hijack the USDC mint authority
Circle's real USDC mint authority can't sign in a test environment. surfnet_setAccount overwrites the mint authority field in the cloned mint account to the test operator's public key — enabling free minting of test USDC without modifying any program logic.
05
Run the full instruction sequence
Tests cover the complete lifecycle: operator init → deposit → staff init → accrue → claim → rebalance → offboard → collect. Keeper bounty logic, vault fallback routing, and liability checkpoint ordering are all tested against real Kamino math.

Read more