This guide explains the programmatic lifecycle utilizing Hastra on Solana. For partners, this process converts liquid capital USDC into wYLDS, which can then be staked into PRIME.
The Hastra Solana protocol consists of two interconnected Anchor programs:
vault-mint(9WUyNREiPDMgwMh5Gt81Fd3JpiCKxpjZ5Dpq9Bo1RhMV)— accepts USDC and issues wYLDS at a 1:1 ratio.
vault-stake(97V7JsExNC6yFWu5KjK1FLfVkNVvtMpAFL5QkLWKEGxY)— accepts wYLDS and issues PRIME based on a Chainlink-sourced exchange rate.
1. The Asset Lifecycle: USDC → wYLDS
When a team "mints" on Hastra Solana, they are depositing USDC into the vault-mint program to receive wYLDS (Wrapped YLDS).
- wYLDS is the base-layer wrapped token on Solana, acting as a custodial wrapper for YLDS, the underlying SEC-registered stablecoin. It acts as a 1:1 receipt for the USDC deposited into the vault token account.
- Context: Unlike a simple swap, this "Deposit" moves USDC into a program-controlled vault token account (PDA-owned), and the program mints wYLDS to the user.
PDA Derivations (vault-mint)
TypeScript (Anchor)
// Derive the essential PDAs for the vault-mint programconst [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")],vaultMintProgramId);const [vaultTokenAccountConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("vault_token_account_config"), configPda.toBuffer()],vaultMintProgramId);const [mintAuthorityPda] = PublicKey.findProgramAddressSync([Buffer.from("mint_authority")],vaultMintProgramId);Programmatic Mint (USDC to wYLDS)
TypeScript (Anchor)
// Anchor program client for vault-mintconst vaultMint = new Program<HastraSolVaultMint>(idl, provider);// Deposit USDC to receive wYLDS.// The program transfers USDC from the user's ATA into the vault token account// and mints wYLDS into the user's wYLDS ATA.await vaultMint.methods.deposit(new BN(amount)).accountsStrict({config: configPda,vaultTokenAccount: vaultUsdcAta, // program-owned USDC vaultvaultTokenAccountConfig: vaultTokenAccountConfigPda,mint: wyldsMint, // wYLDS mintmintAuthority: mintAuthorityPda,signer: userPublicKey,userVaultTokenAccount: userUsdcAta, // user's USDC ATAuserMintTokenAccount: userWyldsAta, // user's wYLDS ATAtokenProgram: TOKEN_PROGRAM_ID,}).rpc();2. The Staking Lifecycle: wYLDS → PRIME
PRIME represents a share-based position in the Democratized Prime pool. Unlike the ETH flow, on Solana yield does not require a manual claim — it accrues through an appreciating Chainlink-sourced exchange rate.
- Context: Yield is generated off-chain on the Provenance side (via the Demo Prime HELOC pool) and bridged back to Solana as YLDS/wYLDS.
- Mechanism: Periodically, the vault-stake program receives newly minted wYLDS via a CPI into vault-mint's
external_program_mintinstruction (publish_rewards). This increases the wYLDS balance held in the stake vault (numerator) without changing the PRIME supply (denominator).
- Result: The PRIME exchange rate appreciates over time. 1 PRIME becomes redeemable for more wYLDS. The on-chain PRIME/wYLDS exchange rate is maintained by Chainlink Data Streams and updated periodically by rewards administrators via
verify_price.
PRIME Exchange Rate:
shares_to_mint = deposit_wYLDS * price_scale / price wYLDS_returned = shares_burned * price / price_scalewhere: price = (wYLDS per 1 PRIME) * price_scale [from Chainlink Data Streams]PDA Derivations (vault-stake)
TypeScript (Anchor)
const [stakeConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("stake_config")],vaultStakeProgramId);const [stakeVaultTokenAccountConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("stake_vault_token_account_config"), stakeConfigPda.toBuffer()],vaultStakeProgramId);const [vaultAuthorityPda] = PublicKey.findProgramAddressSync([Buffer.from("vault_authority")],vaultStakeProgramId);const [mintAuthorityPda] = PublicKey.findProgramAddressSync([Buffer.from("mint_authority")],vaultStakeProgramId);const [stakePriceConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("stake_price_config"), stakeConfigPda.toBuffer()],vaultStakeProgramId);Programmatic Stake (wYLDS to PRIME)
TypeScript (Anchor)
const vaultStake = new Program<HastraSolVaultStake>(idl, provider);// Deposit wYLDS to receive PRIME at the current Chainlink exchange rate.await vaultStake.methods.deposit(new BN(amount)).accountsStrict({stakeConfig: stakeConfigPda,vaultTokenAccount: stakeVaultWyldsAta, // program-owned wYLDS vaultstakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda,vaultAuthority: vaultAuthorityPda,mint: primeMint, // PRIME mintvaultMint: wyldsMint, // wYLDS mintmintAuthority: mintAuthorityPda,signer: userPublicKey,userVaultTokenAccount: userWyldsAta, // user's wYLDS ATAuserMintTokenAccount: userPrimeAta, // user's PRIME ATAstakePriceConfig: stakePriceConfigPda,tokenProgram: TOKEN_PROGRAM_ID,}).rpc();Optional: Merkle-Based Reward Claims(vault-mint)
In addition to exchange-rate appreciation, the protocol supports discrete reward epochs distributed via merkle proofs (used for incentive programs and supplemental allocations).
Important: These claims live on the vault-mint program and mint wYLDS (not PRIME) directly to the user. If a partner wants PRIME exposure on the claimed wYLDS, they must subsequently stake it via vault-stake.
- Context: Each epoch contains a merkle root summarizing user rewards for the period. Rewards are minted as wYLDS directly to the user's wYLDS ATA.
- Leaf format:
sha256(user_pubkey || reward_amount_le_bytes || epoch_index_le_bytes)
// PDA derivations for claimconst [epochPda] = PublicKey.findProgramAddressSync([Buffer.from("epoch"), epochIndexBuffer /* u64 little-endian */],vaultMintProgramId);const [claimRecordPda] = PublicKey.findProgramAddressSync([Buffer.from("claim"), epochPda.toBuffer(), userPublicKey.toBuffer()],vaultMintProgramId);// Claim rewards for a specific epoch with a merkle proof.// Note: this is called on vaultMint, not vaultStake.await vaultMint.methods.claimRewards(new BN(rewardAmount), merkleProof).accountsStrict({config: configPda, // vault-mint config PDAuser: userPublicKey,epoch: epochPda,claimRecord: claimRecordPda, // PDA: ["claim", epoch, user] — prevents double-claimmintAuthority: mintAuthorityPda, // vault-mint mint authoritymint: wyldsMint, // wYLDS mint (not PRIME)userMintTokenAccount: userWyldsAta, // user's wYLDS ATAsystemProgram: SystemProgram.programId,tokenProgram: TOKEN_PROGRAM_ID,}).rpc();3. The Redemption Lifecycle: PRIME → wYLDS → USDC
Returning to USDC is a multi-step process.
Phase 1: Redeem PRIME for wYLDS (vault-stake)
- What happens: The vault-stake program supports a single-step direct redemption. There is no unbonding period or queuing — redeem atomically burns the specified PRIME and transfers the proportional wYLDS from the stake vault to the user.
- Context: The amount of wYLDS returned is calculated using the live Chainlink price at the time of the call:
wYLDS_returned = prime_amount * price / price_scale. The call will fail if the stored Chainlink price is stale (past price_max_staleness) or uninitialised.
TypeScript (Anchor)
// Atomically burn PRIME and receive wYLDS in a single call.await vaultStake.methods.redeem(new BN(primeAmount)).accountsStrict({stakeConfig: stakeConfigPda,vaultTokenAccount: stakeVaultWyldsAta, // program-owned wYLDS vaultstakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda,vaultAuthority: vaultAuthorityPda,signer: userPublicKey,ticket: vaultStakeProgramId, // pass program ID if no legacy v1 ticket existsuserVaultTokenAccount: userWyldsAta, // user's wYLDS ATA (destination)userMintTokenAccount: userPrimeAta, // user's PRIME ATA (source to burn)mint: primeMint, // PRIME mintvaultMint: wyldsMint, // wYLDS mintstakePriceConfig: stakePriceConfigPda,tokenProgram: TOKEN_PROGRAM_ID,}).rpc();Legacy note: An optional
ticket account exists for users who have an UnbondingTicket from the deprecated v1 two-step flow. If no ticket exists, pass the vault-stake program ID for that account; Anchor treats it as None and skips all ticket-related constraints. The on-chain unbonding_period field is permanently set to 0 and the unbond instruction no longer exists.Phase 2: wYLDS → USDC (Operator-Mediated, vault-mint)
To convert wYLDS back to USDC, the vault-mint program uses an admin-mediated two-step
request_redeem / complete_redeem flow. This leg is operator-managed and includes a batching minimum (currently $2k) and may be subject to banking-hours delays during the YLDS → USDC conversion via Circle's CCTP.For most institutional integrators, the recommended path is to stop at wYLDS unless USDC settlement is strictly required.
Phase 2a: User submits a redemption request
const [redemptionRequestPda] = PublicKey.findProgramAddressSync([Buffer.from("redemption_request"), userPublicKey.toBuffer()],vaultMintProgramId);const [redeemVaultAuthorityPda] = PublicKey.findProgramAddressSync([Buffer.from("redeem_vault_authority")],vaultMintProgramId);// Burns wYLDS from the user and creates a RedemptionRequest PDA.await vaultMint.methods.requestRedeem(new BN(wyldsAmount)).accountsStrict({signer: userPublicKey,userMintTokenAccount: userWyldsAta, // user's wYLDS ATA (to burn from)redemptionRequest: redemptionRequestPda,mint: wyldsMint,config: configPda,redeemVaultAuthority: redeemVaultAuthorityPda,systemProgram: SystemProgram.programId,tokenProgram: TOKEN_PROGRAM_ID,}).rpc();Phase 2b: Operator fulfills the redemption
The operator calls complete_redeem once USDC has been bridged via Circle's CCTP. The RedemptionRequest PDA is closed and rent is returned to the user.
// Operator-only: transfers USDC from the redeem vault to the user's wallet.await vaultMint.methods.completeRedeem().accountsStrict({admin: operatorPublicKey,user: userPublicKey,userMintTokenAccount: userWyldsAta, // wYLDS ATA (for constraint validation)userVaultTokenAccount: userUsdcAta, // user's USDC ATA (destination)redemptionRequest: redemptionRequestPda,redeemVaultTokenAccount: redeemVaultUsdcAta,redeemVaultAuthority: redeemVaultAuthorityPda,mint: wyldsMint,config: configPda,tokenProgram: TOKEN_PROGRAM_ID,}).signers([operatorKeypair]).rpc();4. Troubleshooting for "programmatically" Teams
Without a GUI, teams need to know exactly why a transaction failed.
Scenario | Programmatic Check | Logic / Context |
Deposit / Stake Fails | stakeConfig.paused or config.paused on the relevant program | Both vault-mint and vault-stake can be paused by the program authority, blocking deposits and redeems. |
Stake Deposit / Redeem Fails (Price) | stakePriceConfig.priceTimestamp and stakePriceConfig.priceMaxStaleness | vault-stake requires a live Chainlink price. The call fails if the stored price is uninitialised (price_timestamp == 0) or stale (now - price_timestamp > price_max_staleness). Rewards admins must call verify_price to refresh it. |
Account Frozen | Token account frozen by freeze authority | wYLDS and PRIME mints have program-controlled freeze authorities. TRM sanctions rules can auto-freeze addresses violating ToS. |
Redeem Fails (Liquidity) | wYLDS balance of the stake vault | If the stake vault is briefly under-funded (large redemption), the call will fail until the next yield CPI ( publish_rewards) lands. |
request_redeem Fails (Duplicate) | RedemptionRequest PDA already exists | Only one pending redemption request per user is allowed. Complete or wait for the existing one before opening another. |
complete_redeem Fails (Liquidity) | USDC balance of the redeem vault | The off-ramp vault may not yet hold sufficient USDC; the operator controls funding timing. |
Claim Fails (Double Claim) | ClaimRecord PDA already exists | The ClaimRecord PDA at seeds ["claim", epoch, user] is permanent. A given user can only claim each epoch once. |
Claim Fails (Proof) | Merkle proof verification | Verify the leaf is reconstructed as sha256(user_pubkey || amount_le_bytes || epoch_index_le_bytes) and the proof matches the published epoch root. |
5. Key Events / Logs to Monitor
For a full-cycle automated system, your indexer should track instruction logs and account state changes for:
- Deposit (vault-mint): Confirmation that USDC is now wYLDS.
- Deposit (vault-stake): Confirmation that wYLDS is now PRIME.
publish_rewards/ External Program Mint (CPI): Signal that yield was distributed into the stake vault — rewards admins will subsequently callverify_priceto advance the exchange rate.
verify_price(vault-stake): Signal that the on-chain Chainlink price has been updated and the PRIME/wYLDS exchange rate has advanced.
- Redeem (vault-stake): Confirmation that PRIME has been burned and wYLDS returned.
request_redeem (vault-mint): Confirmation that the wYLDS → USDC exit clock has started; track the resultingRedemptionRequestPDA.
complete_redeem(vault-mint): Confirmation that USDC has been bridged back via CCTP and delivered to the user.
create_rewards_epoch(vault-mint): Signal that a new merkle-based wYLDS claim is available for the previous epoch.
Key Differences vs. the ETH Integration
Concept | ETH ( hastra-eth-vault) | Solana ( hastra-sol-vault) |
Yield delivery | Manual claimRewards per epoch with merkle proof | Automatic via Chainlink-tracked PRIME/wYLDS exchange rate (CPI mint into stake vault + verify_price); merkle epochs are supplemental wYLDS rewards |
Token model | Single vault token (wYLDS) | Two-layer: wYLDS (base) + PRIME (share) |
Redemption gate | wYLDS → USDC unbonding via requestRedeem / completeRedeem | PRIME → wYLDS via single-step redeem; wYLDS → USDC is a separate operator-mediated step (request_redeem / complete_redeem) |
Unbonding period | Present | Removed in v0.0.5; redeem is immediate |
Allowlist / KYC | Whitelist on the vault | Permissionless on-chain; sanctions-based freezing only |
Account model | EVM contract calls + ERC-20 approvals | Anchor instructions + PDAs; no approve step (token transfers are signed directly) |
Price oracle | N/A | Chainlink Data Streams — must be periodically refreshed by rewards admins via verify_price before deposits/redeems can proceed |