- Overview
- Decimal Precision
- Mathematical Utilities
- Protocol Flows
- Protocol Explanation
- Build Guide
- Testing Guide
Reserve Folio is a protocol for creating and managing portfolios of SPL-compliant assets entirely onchain. Folios are designed to be used as a single-source of truth for asset allocations, enabling composability of complex, multi-asset portfolios.
Folios support rebalancing via Dutch Auction over an exponential decay curve between two prices. Control flow over the auction is shared between two parties, with a AUCTION_APPROVER approving auctions in advance and a AUCTION_LAUNCHER opening them, optionally providing some amount of additional detail. Permissionless execution is available after a delay.
AUCTION_APPROVER is expected to be the SPL Governance account configured for fast-moving decisions associated with the Folio
AUCTION_LAUNCHER is expected to be a semi-trusted wallet or multisig; They can open auctions within the bounds set by governance, hopefully adding basket definition and pricing precision. If they are offline the auction can be opened permissionlessly after a preset delay. If they are evil, at-best they can deviate rebalancing within the governance-granted range, or prevent a Folio from rebalancing entirely by repeatedly closing-out auctions.
The protocol implements two precision levels for different operations:
- D9 (1e9): Standard Solana token decimal precision
- D18 (1e18): Enhanced precision for complex calculations
- Native precision level for Solana token mints
- Used for all token-related transactions
- Maximum supported decimal precision in protocol
- Used for advanced calculations requiring higher precision
- Handles precision loss mitigation
- Conversion process to D9:
- Values are floored for token operations
- Sub-D9 precision ("dust") is retained
- Accumulated dust compounds into complete D9 units over time
Note: These conventions exclude
state.rs
account definitions to maintain Solidity compatibility.
raw_
: Indicates D9 precision (token amounts)scaled_
: Indicates D18 precision (calculation values)
The math_util.rs
module provides high-precision mathematical operations:
- Basic arithmetic (
add
,sub
,mul
,div
) - Power function (
pow
) using binary exponentiation
Two-tiered approach based on input size:
-
Large Values (n > 1,000,000):
- Taylor series approximation near x = 1
- Three-term series expansion
- Formula: 1 - x/n + (n-1)x²/(2n²) - (n-1)(n-2)x³/(6n³)
-
Standard Values (n ≤ 1,000,000):
- Binary search implementation
- 15 iteration maximum
- Optimized for accuracy/performance balance
Implementation features:
- Input normalization (1 to e range)
- arctanh Taylor series expansion
- Formula: ln((1+x)/(1-x)) = 2 * arctanh(x)
- Epsilon precision: 1e-18
Implementation features:
- Taylor series: e^x = 1 + x + x²/2! + x³/3! + ...
- Dynamic iteration termination
- Bi-directional exponent support
- Epsilon precision: 1e-18
- D18 precision maintenance
- Comprehensive overflow protection
- U256 internal calculations
- Result-based error handling
- Execute
init_folio
- Add tokens via
add_to_basket
(supports multiple transactions for large baskets)Note: Providing
initial_shares
enables immediate minting capability
-
Add tokens to pending basket:
- Use
add_to_pending_basket
for 1-N tokens - Tokens transfer immediately but remain "pending"
- Reversible via
remove_from_pending_basket
- Use
-
Execute minting:
- Call
mint_folio_token
- Maximum 18 tokens without lookup table
- Call
-
Burn tokens:
- Execute
burn_folio_token
- Irreversible operation
- Adds proportional tokens to pending basket
- Execute
-
Claim tokens:
- Use
redeem_from_pending_basket
- Supports multiple transactions for large baskets
- Use
For security, the program is non-upgradeable. Migration process:
- Program owner initiates migration
- Permissionless token transfer
- New version handles account structure migration
- Whitelist-restricted program migration
- Integrated with custom SPL governance program
- Reward-enabled Folios owned by DAO via Governance PDA
- Administrative functions require DAO voting:
- Reward token management
- Ratio configuration
- General administration
A Folio has 3 roles:
OWNER
- Expected: SPL Governance account of Realm with slow configuration
- Can add/remove assets, set fees, configure auction length, set the auction delay, and closeout auctions
- Can configure the
AUCTION_APPROVER
/AUCTION_LAUNCHER
- Primary owner of the Folio
AUCTION_APPROVER
- Expected: SPL Governance account of Relam with fast configuration
- Can approve auctions
AUCTION_LAUNCHER
- Expected: wallet or multisig
- Can open and close auctions, optionally altering parameters of the auction within the approved ranges
- Auction is approved by governance, including an initial price range
- Auction is opened, initiating the progression through the predetermined price curve a. ...either by the auction launcher (immediately, or soon after) b. ...or permissionlessly (after the auction delay passes)
- Bids occur
- Auction expires
Governance configures buy and sell limits for the basket ratios, including a spot estimate:
pub struct BasketRange {
pub spot: u128, /// D18{tok/share}
pub low: u128, /// D18{tok/share} inclusive
pub high: u128, /// D18{tok/share} inclusive
}
sell_limit: BasketRange; // D18{sellTok/share} min ratio of sell tokens in the basket, inclusive
buy_limit BasketRange; // D18{buyTok/share} max ratio of buy tokens in the basket, exclusive
During open_auction
the AUCTION_LAUNCHER
can set the buy and sell limits within the approved ranges provided by governance. If the auction is opened permissionlessly instead, the governance pre-approved spot estimates will be used instead.
There are broadly 3 ways to parametrize [start_price, end_price]
, as the AUCTION_APPROVER
:
- Can provide
[0, 0]
to fully defer to the auction launcher for pricing. In this mode the auction CANNOT be opened permissionlessly. Loss can arise either due to the auction launcher settingstart_price
too low, or due to precision issues from traversing too large a range. - Can provide
[start_price, 0]
to defer to the auction launcher for just theend_price
. In this mode the auction CANNOT be opened permissionlessly. Loss can arise due solely to precision issues only. - Can provide
[start_price, end_price]
to defer to the auction launcher for thestart_price
. In this mode the auction CAN be opened permissionlessly, after a delay. Suggested default option.
The AUCTION_LAUNCHER
can always choose to raise start_price
within a limit of 100x, and end_price
by any amount. They cannot lower either price.
The price range (start_price / end_price
) must be less than 1e9
to prevent precision issues.
Auction lots are sized by Auction.sell_limit
and Auction.buy_limit
. Both correspond to Folio structs about basket ratios that must be maintained throughout the auction:
sell_limit
is the min amount of sell token in the basketD18{sellTok/share}
buy_limit
is the max amount of buy token in the basketD18{buyTok/share}
In general it is possible for the lot
to both increase and decrease over time, depending on whether sell_limit
or buy_limit
is the constraining factor in sizing.
Anyone can bid in any auction in size up to and including the lot
size.
Folios support 2 types of fees. Both have a DAO portion that work the same underlying way, placing limits on how small the fee can be.
Per-unit time fee on AUM
The DAO takes a cut with a minimum floor of 15 bps. A consequence of this is that the Folio always inflates at least 15 bps annually. If the tvl fee is set to 15 bps, then 100% of this inflation goes towards the DAO.
Fee on mints
The DAO takes a cut with a minimum floor of 15 bps. The DAO always receives at least 15 bps of the value of the mint. If the mint fee is set to 15 bps, then 100% of the mint fee is taken by the DAO.
The universal 15 bps fee floor can be lowered by the DAO, as well as set (only lower) on a per Folio basis.
-
Required Repositories:
- Custom SPL Governance: reserve-protocol/solana-program-library
- Repository structure:
parent_directory/ ├── dtfs-solana/ # Main repository └── solana-program-library/ # Custom SPL Governance
-
Development Tools:
# Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Install Solana sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" # Configure PATH echo 'export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"' >> ~/.zshrc source ~/.zshrc # Install Anchor cargo install --git https://github.com/coral-xyz/anchor avm --force avm install 0.30.1 avm use 0.30.1
-
Configuration Files:
- Copy
.env.example
to.env
- Configure required variables:
ADMIN_PUBKEY=AXF3tTrMUD5BLzv5Fmyj63KXwvkuGdxMQemSJHtTag4j SPL_GOVERNANCE_PROGRAM_ID=HwXcHGabc19PxzYFVSfKvuaDSNpbLGL8fhVtkcTyEymj
- Copy
-
Key Management:
Note: Local Development keys are included in
/utils/keys/
folio_admin-keypair-local.json
: Folio Admin program IDfolio-keypair-local.json
: Folio Primary program IDfolio-2-keypair-local.json
: Folio Secondary program IDspl-governance-keypair.json
: Custom Governance program IDkeys.json
: Test configuration keys
Configure Anchor.toml
for target environment:
[provider]
cluster = "Localnet" # Options: Localnet, Devnet, Mainnet
wallet = "~/.config/solana/id.json"
[programs.localnet]
folio = "n6sR7Eg5LMg5SGorxK9q3ZePHs9e8gjoQ7TgUW2YCaG"
folio_admin = "7ZqvG9KKhzA3ykto2WMYuw3waWuaydKwYKHYSf7SiFbn"
Execute build-local.sh
:
- SPL Governance compilation
- Dual Folio program builds:
- Standard instance
- Development instance (feature-enabled) (can be migrated to for testing purposes)
# Generate new keypairs
solana-keygen new -o folio_admin-keypair.json --no-bip39-passphrase
solana-keygen new -o folio-keypair.json --no-bip39-passphrase
# View public keys
solana address -k target/deploy/folio_admin-keypair.json
solana address -k target/deploy/folio-keypair.json
Important: Backup deployment keys for devnet/mainnet environments to maintain upgrade capability
The protocol implements three distinct testing approaches to ensure comprehensive coverage and reliability.
Located under the tests/
directory, these tests focus on pure Rust functions without Anchor-specific parameters.
- Tests functions without
Account
,AccountInfo
, orAccountLoader
parameters - Fast execution and simple setup
- Ideal for testing mathematical operations and utility functions
# Standard test execution
cargo test
# With coverage reporting
cargo tarpaulin --workspace \
--exclude-files \
"programs/*/src/instructions/*" \
"programs/*/src/external/*" \
"programs/*/src/**/events.rs" \
"programs/*/src/**/state.rs" \
"programs/*/src/lib.rs" \
"programs/*/src/**/errors.rs" \
--out Html \
--output-dir target/tarpaulin
Note: Some functions are intentionally excluded from coverage metrics due to the complexity of mocking Anchor structures. These typically involve Account-related parameters.
Located under tests-ts/bankrun/
, these tests provide a middle ground between unit and integration tests.
- Simulates on-chain behavior without requiring a validator
- Faster than full integration tests
- Tests program instructions and account interactions
# 1. Build programs
./build-local.sh
# 2. Download required programs
./download-programs.sh
# 3. Run tests
anchor run test-bankrun
Note: Bankrun tests don't require a Solana test validator, making them significantly faster than traditional integration tests.
Located under tests-ts/tests-*.ts
, these tests provide full end-to-end testing of the protocol.
- Complete on-chain interaction testing
- Tests cross-program interactions
- Validates real-world scenarios
-
Start the test environment:
# Terminal 1 ./start-amman.sh
-
Run the tests:
# Terminal 2 tsc && anchor test --skip-local-validator --skip-deploy --skip-build
- Uses Amman for transaction monitoring
- Skips local validator (using Amman instead)
- Skips deployment and build (assuming programs are pre-built)
project/
├── tests/ # Rust unit tests
├── tests-ts/
│ ├── bankrun/ # Bankrun tests
│ │ └── test-runner.ts
│ └── tests-*.ts # Integration tests
-
Unit Tests
- Write for all pure functions
- Focus on edge cases and error conditions
- Maintain high coverage for non-Account functions
-
Bankrun Tests
- Test instruction logic
- Validate account constraints
- Verify state transitions
-
Integration Tests
- Test complete user flows
- Verify cross-program interactions
- Validate real-world scenarios
classDiagram
class Folio {
+bump: u8
+status: u8
+folio_token_mint: Pubkey
+tvl_fee: u128
+mint_fee: u128
+dao_pending_fee_shares: u128
+fee_recipients_pending_fee_shares: u128
+auction_delay: u64
+auction_length: u64
+current_auction_id: u64
+last_poke: i64
+mandate: FixedSizeString
}
class Actor {
+bump: u8
+authority: Pubkey
+folio: Pubkey
+roles: u8
}
class FolioBasket {
+bump: u8
+folio: Pubkey
+token_amounts: TokenAmount[]
}
class UserPendingBasket {
+bump: u8
+owner: Pubkey
+folio: Pubkey
+token_amounts: TokenAmount[]
}
Folio --> Actor: has many
Folio --> FolioBasket: has one
Folio --> UserPendingBasket: has many
classDiagram
class Auction {
+bump: u8
+id: u64
+available_at: u64
+launch_timeout: u64
+start: u64
+end: u64
+k: u128
+folio: Pubkey
+sell: Pubkey
+buy: Pubkey
+sell_limit: BasketRange
+buy_limit: BasketRange
+prices: Prices
}
Folio --> Auction: has many
classDiagram
class FeeRecipients {
+bump: u8
+distribution_index: u64
+folio: Pubkey
+fee_recipients: FeeRecipient[]
}
class FeeDistribution {
+bump: u8
+index: u64
+folio: Pubkey
+cranker: Pubkey
+amount_to_distribute: u128
+fee_recipients_state: FeeRecipient[]
}
class DAOFeeConfig {
+bump: u8
+fee_recipient: Pubkey
+default_fee_numerator: u128
+default_fee_floor: u128
}
class FolioFeeConfig {
+bump: u8
+fee_numerator: u128
+fee_floor: u128
}
Folio --> FeeRecipients: has one
FeeRecipients --> FeeDistribution: has many
DAOFeeConfig --> FolioFeeConfig: configures
FolioFeeConfig --> Folio: configures
classDiagram
class FolioRewardTokens {
+bump: u8
+folio: Pubkey
+reward_ratio: u128
+reward_tokens: Pubkey[]
+disallowed_token: Pubkey[]
}
class RewardInfo {
+bump: u8
+folio: Pubkey
+folio_reward_token: Pubkey
+payout_last_paid: u64
+reward_index: u128
+balance_accounted: u128
+balance_last_known: u128
+total_claimed: u128
}
class UserRewardInfo {
+bump: u8
+folio: Pubkey
+folio_reward_token: Pubkey
+last_reward_index: u128
+accrued_rewards: u128
}
Folio --> FolioRewardTokens: has one
FolioRewardTokens --> RewardInfo: has many
RewardInfo --> UserRewardInfo: has many
classDiagram
class ProgramRegistrar {
+bump: u8
+accepted_programs: Pubkey[]
}
ProgramRegistrar --> Folio: registers versions
sequenceDiagram
participant Owner as Folio Owner (DAO)
participant Approver as Auction Approver
participant Launcher as Auction Launcher
participant User
participant Folio
%% Initialization Flow
rect rgb(106, 106, 116)
Note over Owner, Folio: Initialization Phase
Owner->>Folio: init_folio()
Owner->>Folio: init_or_update_actor(AUCTION_APPROVER)
Owner->>Folio: init_or_update_actor(AUCTION_LAUNCHER)
Owner->>Folio: add_to_basket(initial_tokens)
end
%% Auction Flow
rect rgb(91, 81, 81)
Note over Approver, Folio: Auction Phase
Approver->>Folio: approve_auction(price_range, limits)
alt Immediate Launch
Launcher->>Folio: open_auction()
else After Delay
User->>Folio: open_auction_permissionless()
end
User->>Folio: bid()
opt If needed
Launcher->>Folio: close_auction()
end
end
%% User Operations
rect rgb(52, 55, 52)
Note over User, Folio: User Operations
User->>Folio: add_to_pending_basket()
User->>Folio: mint_folio_token()
User->>Folio: burn_folio_token()
User->>Folio: redeem_from_pending_basket()
end
%% Reward Management
rect rgb(42, 36, 36)
Note over Owner, Folio: Reward Management
Owner->>Folio: add_reward_token()
User->>Folio: accrue_rewards()
User->>Folio: claim_rewards()
end
%% Fee Management
rect rgb(32, 36, 36)
Note over User, Folio: Fee Distribution
User->>Folio: poke_folio()
User->>Folio: distribute_fees()
User->>Folio: crank_fee_distribution()
end