DEV Community

Divine Igbinoba
Divine Igbinoba

Posted on

Atlas Payroll: Streaming Payroll with Operational Capital Yield on Solana



Introduction

Atlas Payroll is a Solana smart contract that reimagines how corporate payroll and idle operational capital are managed. Instead of letting company funds sit dormant in a commercial bank account earning little to no interest, Atlas routes those funds into Kamino Finance (a Solana native lending protocol) to earn yield on the deposited capital. At the same time, employees are paid in real time. Their salary accrues every second and can be withdrawn at any point.

While this architecture is general enough to be applied in other contexts, it was designed specifically with corporate payroll and operational treasury management in mind, giving employers a way to put idle operational capital to work whilst giving employees liquidity on demand.


System Overview

The protocol revolves around five participants:

  1. The Capital Provider (Operator): Deposits funds that are routed to Kamino Finance to earn yield. In a corporate context, this is the employer or treasury manager.

  2. The Recipient (Employee): Pay accrues per second and can be withdrawn at any time.

  3. Kamino Finance: The external lending protocol that earns yield on the deposited USDC.

  4. The Safety Vault: An on-chain USDC token account that holds at least two days' worth of total payroll at all times, providing a liquidity buffer in case Kamino is temporarily illiquid.

  5. The Keeper: An off-chain agent that monitors employee claim, operator withdrawal, and staff offboarding events to infer when the safety vault balance has been reduced. When a rebalance is warranted, it calls the rebalance instruction and earns a $1 USDC bounty per successful call.

Operational Flow

  1. The operator initialises their protocol vault account on-chain.

  2. The operator deposits USDC from an external wallet; the contract routes it directly into Kamino Finance, receiving kUSDC (Kamino's yield bearing collateral token) in return.

  3. The operator initialises employee accounts, specifying each employee's annual salary. The contract derives the per second pay rate and begins accruing salary from that moment.

  4. The keeper monitors the vault balance off-chain. When it detects that the safety vault has fallen below the two-day payroll threshold, it calls the rebalance instruction, which withdraws enough USDC from Kamino to top up the vault, and earns its bounty.

  5. The operator may withdraw capital from Kamino at any time, subject to the constraint that they cannot withdraw funds that are owed to employees (i.e., the total liability is always preserved).

  6. Employees call the claim instruction to withdraw their accrued salary. Funds are pulled from the safety vault first; if the vault balance is insufficient, the shortfall is sourced directly from Kamino.

  7. When an employee is offboarded, all outstanding pay is sent to them immediately, their accrual stops, and their rate is removed from the global payroll rate.

  8. Once an employee account has a zero outstanding balance, the operator can close it and reclaim the on-chain rent (SOL storage deposit).


Architecture

The protocol is divided into three logical sub systems:

  • Operator architecture: Deposit, withdraw, and capital management.

  • Employee architecture: Salary accrual, claim, offboarding, and account closure.

  • Rebalance architecture: Moving funds from Kamino yield into the safety vault.


Technical Stack

Atlas Payroll is written in Rust using the Anchor framework (v0.32.1) and deployed on the Solana blockchain.

key dependencies (Cargo.toml):

[package]
name = "anchor-payroll-capstone-q1-26"
version = "0.1.0"
edition = "2021"

[dependencies]
anchor-lang    = { version = "0.32.1", features = ["init-if-needed"] }
anchor-spl     = { version = "0.32.1" }
solana-program = "2.3.0"
bytemuck       = { version = "1.20.0", features = ["min_const_generics"] }
Enter fullscreen mode Exit fullscreen mode

Toolchain requirements: Rust (stable), Solana CLI, Anchor CLI v0.32.1, Node.js (for tests), and Surfpool (for local testnet with mainnet account cloning).


Contract Data Structures

General Notes on Types

All account public keys are stored as Pubkey (32 byte unsigned integers). All token amounts are stored as u64 (unsigned 64 bit integers), which can represent values up to approximately 1.8 × 10¹⁹. This comfortably accommodates USDC amounts expressed in their smallest unit (micro-USDC, 6 decimal places), allowing the contract to represent hundreds of millions of dollars without overflow.


1. The Operator Account (ProtocolVault)

Fields Type and Description

update_liability (Update Liability)

This method is called at the beginning of most instructions to ensure the recorded liability is current before any financial logic is executed:

new_liability = liability + (global_rate × (current_time - liability_timestamp))
liability_timestamp = current_time
Enter fullscreen mode Exit fullscreen mode

update_protocol (Safety Vault Requirement Check)

This method calculates how much USDC needs to be transferred from Kamino into the safety vault in order to bring the vault back up to the required two day buffer:

required = (global_rate × 2.2 days in seconds) - safety_amount
Enter fullscreen mode Exit fullscreen mode

The 2.2-day multiplier (rather than exactly 2.0) provides a small operational buffer to account for network latency, keeper scheduling delays, and minor fluctuations.

calculate_k_pool (Kamino Pool Valuation)

This is one of the more technically complex parts of the contract. It reads the Kamino Reserve account directly from raw bytes using zero-copy deserialisation (via bytemuck) to avoid the compute cost of standard Anchor deserialisation.

It returns a tuple of (total_pool_usdc, total_ktoken), representing the total USDC liquidity in the Kamino reserve and the total supply of kUSDC tokens respectively. These two values form the exchange rate used to convert between kUSDC and USDC throughout the contract.

Important implementation detail

Kamino stores its liquidity values using a u68.60 fixed point format (also referred to as "scaled fraction" (_sf ) suffix in field names).

The scale factor is 2^60 = 1,152,921,504,606,846,976, which is different from the standard Solana convention of 10^18. This is a critical distinction: using the wrong WAD value would produce wildly incorrect exchange rate calculations.

Source: deepwiki.com/Kamino-Finance/klend

Kamino's IDL defines several liquidity fields as u128 (e.g., borrowed_amount_sf, accumulated_protocol_fees_sf). However, because Solana on-chain accounts begin with an 8 byte discriminator, subsequent fields are statistically more likely to be 8 byte aligned than 16 byte aligned.

Since bytemuck requires strict alignment guarantees that cannot be assured for u128 (which requires 16 byte alignment) in this context, these fields are split into two u64 values in the contract's struct definition and then reconstructed into a u128 at runtime:

Source: github.com/solana-foundation/anchor/issues/3114

The total USDC in the pool is then computed as:

total_pool_usdc = (available_amount + borrowed_amount - protocol_fees - referrer_fees - pending_referrer_fees) / wad
Enter fullscreen mode Exit fullscreen mode

calculate_total_assets (Net Available Capital)

This function computes the total USDC value the operator can access, net of all employee liabilities:

It converts the protocol's kUSDC balance to its USDC equivalent using the pool ratio, adds the safety vault balance, and then subtracts the current total liability (recalculated to the present moment using global_rate and liability_timestamp). This is the number checked before allowing any operator withdrawal.

ktoken_to_burn (Redemption Calculation)

Given a required USDC amount, this function calculates the exact number of kUSDC tokens that must be redeemed from Kamino to receive at least that amount:

ktoken_to_burn = ceil(usdc_required × total_ktoken / total_pool_usdc)
Enter fullscreen mode Exit fullscreen mode

Ceiling division is used deliberately to ensure the protocol never under redeems. Without rounding up, integer truncation could result in receiving slightly less USDC than required:

The current mint_total_supply (total kUSDC in circulation) is read via manual byte offset deserialisation because this specific field is not always accessible without unpacking the full reserve struct:


2. The Employee Account (StaffAccount)

Fields Type and Description

claimable_salary

This calculates the amount the employee can currently withdraw:

effective_time = if !active && time_ended != 0 { time_ended } else { now }
claimable = (effective_time - time_started) × rate - total_claimed
Enter fullscreen mode Exit fullscreen mode

When an employee has been offboarded (active = false), time_ended is used as the cutoff ensuring no further salary accrues after offboarding, while still allowing the employee to withdraw any remaining unclaimed balance.


3. Kamino's Reserve Account (Reserve)

The Kamino Reserve struct is a large on-chain account maintained by the Kamino lending protocol. This contract does not own or write to it; it reads from it to obtain the data needed for exchange rate calculations.

Fields of relevance to this contract:


Helpers

Instruction Discriminator

Anchor programs identify instructions by an 8 byte discriminator derived from the global namespace prefix and the instruction name. Since this contract calls Kamino via raw CPI (Cross Program Invocation) rather than through an Anchor generated client, it must construct this discriminator manually:

Key Constants


Instructions

1. Operator Initialisation (OperatorInit)

This instruction creates and initialises the ProtocolVault account for a given operator. A single operator can only ever have one protocol vault. The PDA seed is derived solely from the operator's public key, making it impossible to initialise a second vault with the same wallet:

This enforces a one to one relationship between an employer and their payroll contract instance, preventing duplicate accounts and simplifying account resolution.

Accounts required:

  • operator: Signer and rent payer.

  • protocol: The PDA account to be initialised (ProtocolVault).

The following are solana system level programs required by almost every instruction in this contract:

  • system_program : Solana's built-in program responsible for the fundamental account lifecycle.

  • token_program : holds exclusive write authority over all SPL token accounts (ATAs)

  • associated_token_program: derives and creates the associated token account (ATA) address for a given wallet and mint

TokenInterface is used instead of Program<Token> to maintain compatibility with both the original SPL Token program and the newer Token-2022 program:

Instruction logic:

The protocol vault is initialised with all amounts set to zero and liability_timestamp set to the current clock time, establishing the baseline for future liability calculations. set_inner is Anchor's method for serialising a fully constructed struct into an account in one atomic operation:


2. Operator Deposit (Deposit)

The operator deposits USDC from their wallet into Kamino Finance. The contract constructs and fires a CPI call to Kamino's deposit_reserve_liquidity instruction, which transfers USDC from the operator's associated token account and mints kUSDC into the protocol's kUSDC token account.

Key accounts:

  • operator: Signer (the account paying USDC into the pool).

  • protocol_authority: Derived from [b"authority", protocol.key()]. This PDA is the program-controlled authority over all protocol owned token accounts (USDC and kUSDC):

  • protocol_ktoken_ata: The protocol's associated token account for kUSDC. Kamino deposits collateral tokens here after each deposit:

  • instruction_sysvar: Kamino performs introspection on the current transaction's instruction set as a security check. This account must be present:

Kamino specific accounts:

These accounts are required by Kamino's program and are passed through with /// CHECK: since Anchor does not natively own or validate them, Kamino's program enforces their correctness on its side.

  • kamino_program: The on-chain program ID of Kamino Finance. This is the address of the deployed Kamino lending program on Solana mainnet. Source: github.com/Kamino-Finance/klend

  • reserve: The address of the USDC Kamino reserve account. This account holds all state for the lending market including ReserveCollateral (kUSDC supply data) and ReserveLiquidity (USDC liquidity data). It is the account this contract reads from in calculate_k_pool.

  • lending_market: The specific Kamino lending market this contract interfaces with, in this case the main USDC market.

  • reserve_liquidity_mint: The mint address of the token used in this market, which is USDC. This tells Kamino which token is being deposited.

  • reserve_liquidity_supply: The address of the total liquidity supply pool for this reserve. This is the Kamino controlled vault that holds all deposited USDC.

  • reserve_collateral_mint: The mint address of the yield bearing collateral token Kamino issues in exchange for a deposit (kUSDC). This token is the mechanism through which yield is realised: as borrowers pay interest into the pool, the kUSDC-to-USDC exchange rate increases, meaning the same kUSDC balance becomes redeemable for more USDC over time. Returning kUSDC to this mint via redeem_reserve_collateral burns it and releases the underlying USDC plus accrued interest.

Why **Box<InterfaceAccount<...>>?** Solana's BPF runtime enforces a 4KB stack limit per frame. Wrapping large account types in Box<> heap-allocates them, preventing stack overflows when multiple large accounts are in scope simultaneously.

Instruction logic:

update_liability() is called first to ensure the protocol's employee debt record is current before any funds move. The kUSDC balance is snapshotted before the CPI, then diffed against the post-CPI balance after reload() to determine exactly how many kUSDC tokens Kamino minted. Without reload(), the in-memory account struct would still reflect the pre-CPI balance:

Why **invoke vs invoke_signed?*invoke *is used here because the operator is the top-level signer and is already declared in the accounts struct. invoke_signed is required only when a PDA needs to act as a signer. The seeds must be provided so the runtime can verify the PDA derivation before granting signing authority:

Why are accounts listed twice?AccountMeta carries the metadata Solana needs to validate accounts and calculate the compute budget (public key, writable flag, signer flag). The raw AccountInfo slice passed to invoke carries the actual account data (lamports, data bytes, owner, and the executable flag). Both are required:


3. Operator Withdrawal (CFOWithdraw)

The operator can withdraw USDC from the protocol's capital at any time, subject to one hard constraint: the total amount owed to all employees is always preserved. calculate_total_assets returns net available capital, the total USDC value of all assets minus current liability and any withdrawal request beyond this ceiling fails immediately.

Instruction logic:

If the requested amount exceeds what is currently in the safety vault, the shortfall is first redeemed from Kamino before the final transfer. The safety vault is always the first source because it is the most immediately liquid. Redeeming from Kamino incurs additional compute cost and potential minor slippage at scale:

invoke_signed is used for the Kamino redemption (unlike deposit) because protocol_authority must sign the redemption on the protocol's behalf. The signer seeds are constructed and passed as follows:

debit_safety uses these same seeds to authorise the final USDC transfer from the protocol's token account to the operator's wallet:


4. Rebalance (Rebalance)

The rebalance instruction is called by the keeper (an off-chain monitoring agent) which watches for employee claim, operator withdrawal, and staff offboarding events to infer when the safety vault has likely been drawn down. The keeper earns a fixed $1 USDC bounty per successful call, and the platform earns a 0.5% fee on the total amount moved from Kamino.

Key accounts (additions over withdrawal):

  • keeper: Signer. The off-chain agent calling the instruction.

  • keeper_ata: The keeper's USDC token account, where the bounty is sent.

  • platform: The platform treasury public key.

  • platform_ata: The platform's USDC token account, where the 0.5% tax is sent.

Instruction logic:

update_protocol() computes the USDC deficit: how much needs to move from Kamino to restore the full two-day buffer. This required amount is capped at max_claimable (the USDC equivalent of the protocol's entire kUSDC balance) to prevent over redemption:

The bounty and tax are then computed:

Two guards protect the keeper from wasted fees. The first fires before any Kamino interaction so that if the vault is already adequately funded, or if the available yield is smaller than the total cost to rebalance, the instruction returns Ok(()) silently. The second fires after the redemption, in case the USDC actually received from Kamino falls short due to rounding:

After both guards pass, the bounty and tax are dispatched and the remainder is credited to protocol.safety_amount:


5. Staff Initialisation (StaffInit)

The operator initialises an employee account, specifying the employee's annual salary in micro-USDC. Annual salary is the standard unit in employment contracts; the contract converts it to a per second rate internally:

rate_per_second = annual_salary / 31,557,600
Enter fullscreen mode Exit fullscreen mode

31,557,600 is the number of seconds in an average Gregorian year (365.25 × 86,400), which correctly accounts for leap years.

The PDA seed is derived from the employee's wallet address, meaning each employee can only ever have one active account per protocol. The payer = operator constraint gives the operator authority to later close the account and reclaim the rent:

Instruction logic:

update_liability() is called first to checkpoint the existing accumulated debt before adding the new employee's rate. If this step were skipped, the new rate would retroactively inflate the liability calculation back to liability_timestamp, over-counting what the protocol owes. The per second rate is then added to protocol.global_rate, immediately factoring into all future liability calculations:


6. Staff Claim (StaffClaim)

An employee calls this instruction to withdraw their accrued salary at any time.

Instruction logic:

claimable_salary() is called on the StaffAccount to compute the exact amount owed. If the claimable amount exceeds the safety vault balance, the shortfall is sourced directly from Kamino rather than failing the transaction. The protocol prioritises ensuring employees receive their pay even when the keeper has not rebalanced recently:

A MINIMUM_CLAIM guard protects against "gas attrition" a scenario where the transaction fee to execute the claim exceeds the value being claimed, leaving the employee worse off.

If the claimable amount falls below this threshold the claim reverts, and the employee is advised to wait for additional accrual. staff_account.total_claimed is then incremented to track the employee's cumulative lifetime withdrawals, and protocol.liability is decremented by the claimed amount to keep the liability register accurate:


7. Staff Offboarding (‎StaffOffboard)

The operator calls this instruction to terminate an employee's salary accrual and send all outstanding pay in a single atomic transaction.

The account struct includes a constraint = staff_account.active == true guard, making it impossible to accidentally offboard an employee twice. active and time_ended are updated before the payment logic executes, so even in the illiquid edge case the employee's accrual is permanently stopped while any unpaid balance remains claimable via staff_claim:

Instruction logic:

All remaining pay is then transferred to the employee using the same vault-first, Kamino fallback logic as staff_claim. In the rare edge case where neither the vault nor Kamino has sufficient USDC to cover the full final payment, the employee is offboarded and a warning is emitted keeping the remaining balance claimable:


8. Collect Staff (CollectStaff‎)

Once an employee has been offboarded and has claimed all outstanding pay, the operator closes the StaffAccount on-chain, freeing storage and reclaiming the SOL rent deposit.

Instruction logic:

claimable_salary() is the critical safety gate ensuring that if any balance remains unclaimed, the close is rejected entirely. This ensures the operator cannot inadvertently destroy an account that still holds an employee's earned funds:

The close = operator Anchor constraint handles the rest: once the instruction succeeds, Anchor zeroes out the account data and transfers all remaining lamports to the operator's wallet at the end of the transaction:


Testing Strategy

Testnet Setup with Surfpool

Testing this contract against Kamino is non-trivial because Kamino's lending markets only exist on mainnet. The approach taken here is to clone the mainnet state into a local test environment using Surfpool (a Solana test validator that supports cloning mainnet accounts).

Finding the USDC Kamino Reserve:

The test setup queries the mainnet RPC using getProgramAccounts with two memcmp filters, one matching the target lending market address at byte offset 32, and one matching the USDC mint at byte offset 128 (the positions of lending_market and liquidity_mint respectively within the Reserve struct). This returns the exact reserve account without needing to know its address in advance:

Deriving dependent addresses from the cloned reserve:

LENDING_MARKET_AUTHORITY is derived via findProgramAddressSync using the lma seed prefix that Kamino uses for all lending market authority PDAs:

RESERVE_LIQUIDITY_SUPPLY and RESERVE_COLLATERAL_MINT are read directly from the cloned reserve account data using their known byte offsets within the Reserve struct:

Hijacking the USDC mint:

Because the cloned mainnet USDC mint has a fixed mint authority (controlled by Circle), the test must override the authority to enable minting test tokens. This is done via Surfpool's surfnet_setAccount JSON-RPC method, which allows arbitrary account state to be injected into the test validator. The mint authority field is overwritten in-memory with the operator's public key and pushed back:

Resetting the reserve's stale slot counter:

Kamino's deposit_reserve_liquidity instruction performs a freshness check because if the reserve has not been refreshed within the current slot, the deposit is rejected. Since the cloned reserve reflects a past mainnet state, this check would always fail in a test environment. The last updated slot counter at byte offset 16 in the reserve data is zeroed out before the tests run, bypassing this check:

Test Cases


Running the Contract

Build

anchor build
Enter fullscreen mode Exit fullscreen mode

Start the local test validator (Surfpool)

surfpool --start
Enter fullscreen mode Exit fullscreen mode

Run Tests

anchor test --skip-local-validator
Enter fullscreen mode Exit fullscreen mode

On some WSL setups, the deploy path inside anchor test can be slower or unreliable relative to Surfpool's blockhash expiry window, which may cause Blockhash not found / Blockhash expired errors. In that case, deploy explicitly first, then run tests without redeploying:

Deploy

solana program deploy target/deploy/anchor_payroll_capstone_q1_26.so\
  --program-id target/deploy/anchor_payroll_capstone_q1_26-keypair.json\
  -u localhost\
  --use-rpc
Enter fullscreen mode Exit fullscreen mode

This command talks directly to the local validator's JSON‑RPC on localhost:8899, avoiding the extra orchestration overhead of anchor test during deployment and making it less likely the blockhash expires mid‑upload.

Run Tests

anchor test --skip-local-validator --skip-deploy
Enter fullscreen mode Exit fullscreen mode

This runs the tests against your already‑deployed program and skips the deploy step entirely.


Atlas Payroll was built as a Q1 2026 capstone project. The contract targets Solana mainnet-beta, with the Kamino Finance USDC lending market (KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD) as the yield source. Full source code: github.com/DivineUX23/anchor-payroll-capstone-q1-26

Top comments (0)