From 43822af6479dbb4fc372aa1e236accb3f7499ffc Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 1 Feb 2024 13:54:21 +0100 Subject: [PATCH 1/8] Marinade SP: refactor calls to marinade liquid unstake - including a neater way to calculate the seeds. --- .../sdks/common/src/types/marinade_beam.ts | 96 +++++++++++++++- packages/sdks/marinade-sp/src/index.ts | 68 ++++++++--- packages/sdks/spl/src/index.ts | 2 - .../src/functional/beams/marinade-sp.test.ts | 84 +++++++------- .../src/cpi_interface/marinade.rs | 99 ++++++++++------ .../marinade-beam/src/cpi_interface/mod.rs | 2 + .../src/cpi_interface/program.rs | 11 ++ .../src/cpi_interface/sunrise.rs | 33 +++++- .../src/cpi_interface/vault_authority_seed.rs | 27 +++++ programs/marinade-beam/src/lib.rs | 108 ++++++++++++------ programs/sunrise-core/src/lib.rs | 3 +- 11 files changed, 389 insertions(+), 144 deletions(-) create mode 100644 programs/marinade-beam/src/cpi_interface/program.rs create mode 100644 programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 3b41d32..aedc258 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -807,12 +807,56 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "epochReportAccount", + "name": "liqPoolSolLegPda", "isMut": true, "isSigner": false }, { - "name": "clock", + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryMsolAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "epochReport", + "isMut": true, + "isSigner": false, + "docs": [ + "The epoch report account. This is updated with the latest extracted yield value.", + "It must be up to date with the current epoch. If not, run updateEpochReport before it." + ] + }, + { + "name": "sysvarClock", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false } @@ -1787,12 +1831,56 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "epochReportAccount", + "name": "liqPoolSolLegPda", "isMut": true, "isSigner": false }, { - "name": "clock", + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryMsolAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "epochReport", + "isMut": true, + "isSigner": false, + "docs": [ + "The epoch report account. This is updated with the latest extracted yield value.", + "It must be up to date with the current epoch. If not, run updateEpochReport before it." + ] + }, + { + "name": "sysvarClock", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false } diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index ed7e3ae..88a1fb1 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -8,6 +8,7 @@ import { SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, StakeProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -236,27 +237,29 @@ export class MarinadeClient extends BeamInterface< gsolTokenAccount, ); + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + withdrawer, + gsolTokenAccount: burnGsolFrom, + gsolMint, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinade.state.solLeg(), + liqPoolMsolLeg: this.marinade.state.mSolLeg, + treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, + instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + console.log(accounts); const instruction = await this.program.methods .withdraw(amount) - .accounts({ - state: this.stateAddress, - marinadeState: this.state.proxyState, - sunriseState: this.state.sunriseState, - withdrawer, - gsolTokenAccount: burnGsolFrom, - gsolMint, - msolMint: this.marinade.state.mSolMint.address, - msolVault: this.marinade.beamMsolVault, - vaultAuthority: this.vaultAuthority[0], - liqPoolSolLegPda: await this.marinade.state.solLeg(), - liqPoolMsolLeg: this.marinade.state.mSolLeg, - treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, - instructionsSysvar, - sunriseProgram: this.sunrise.program.programId, - marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - }) + .accounts(accounts) .instruction(); return new Transaction().add(instruction); @@ -477,4 +480,31 @@ export class MarinadeClient extends BeamInterface< const PID = programId ?? MARINADE_BEAM_PROGRAM_ID; return Utils.deriveStateAddress(PID, sunriseState); }; + + /** + * Return a transaction to extract any yield from this beam into the yield account + */ + public async extractYield(): Promise { + const instruction = await this.program.methods + .extractYield() + .accounts({ + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinade.state.solLeg(), + liqPoolMsolLeg: this.marinade.state.mSolLeg, + treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction(); + + return new Transaction().add(instruction); + } } diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 6315f45..e230891 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -459,8 +459,6 @@ export class SplClient extends BeamInterface { systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }; - console.log("EXTRACT YIELD ACCOUNTS"); - console.log(accounts); const instruction = await this.program.methods .extractYield() .accounts(accounts) diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 457fd48..1a05380 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -13,6 +13,7 @@ import { import BN from "bn.js"; import { createTokenAccount, + expectSolBalance, expectStakerSolBalance, expectTokenBalance, fund, @@ -41,7 +42,7 @@ describe("Marinade stake pool beam", () => { const failedDepositAmount = 5 * LAMPORTS_PER_SOL; const liquidWithdrawalAmount = 5 * LAMPORTS_PER_SOL; const delayedWithdrawalAmount = 5 * LAMPORTS_PER_SOL; - // const burnAmount = new BN(1 * LAMPORTS_PER_SOL); + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -298,45 +299,44 @@ describe("Marinade stake pool beam", () => { ); }); - // - // it("can burn gsol", async () => { - // // burn some gsol to simulate the creation of yield - // await sendAndConfirmTransaction( - // stakerIdentity, - // await beamClient.burnGSol(burnAmount), - // ); - // - // const expectedGsol = stakerGsolBalance.sub(burnAmount); - // - // await expectTokenBalance( - // beamClient.provider, - // beamClient.sunrise.gsolAssociatedTokenAccount(), - // expectedGsol, - // ); - // }); - // - // it("can extract yield into a stake account", async () => { - // // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) - // // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. - // - // await sendAndConfirmTransaction( - // // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test - // // to show that it doesn't have to be an admin - // stakerIdentity, - // await beamClient.extractYield(), - // ); - // - // // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account - // const expectedFee = new BN(0); // TODO - // const expectedExtractedYield = burnAmount.sub(expectedFee); - // - // await expectSolBalance( - // beamClient.provider, - // beamClient.sunrise.state.yieldAccount, - // expectedExtractedYield, - // // // the calculation appears to be slightly inaccurate at present, but in our favour, - // // // so we can leave this as a low priority TODO to improve the accuracy - // // 3000, - // ); - // }); + it("can burn gsol", async () => { + // burn some gsol to simulate the creation of yield + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.burnGSol(burnAmount), + ); + + const expectedGsol = stakerGsolBalance.sub(burnAmount); + + await expectTokenBalance( + beamClient.provider, + beamClient.sunrise.gsolAssociatedTokenAccount(), + expectedGsol, + ); + }); + + it("can extract yield into a stake account", async () => { + // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) + // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. + + await sendAndConfirmTransaction( + // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.extractYield(), + ); + + // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account + const expectedFee = new BN(0); // TODO + const expectedExtractedYield = burnAmount.sub(expectedFee); + + await expectSolBalance( + beamClient.provider, + beamClient.sunrise.state.yieldAccount, + expectedExtractedYield, + // // the calculation appears to be slightly inaccurate at present, but in our favour, + // // so we can leave this as a low priority TODO to improve the accuracy + // 3000, + ); + }); }); diff --git a/programs/marinade-beam/src/cpi_interface/marinade.rs b/programs/marinade-beam/src/cpi_interface/marinade.rs index 8d239e1..e2e15e9 100644 --- a/programs/marinade-beam/src/cpi_interface/marinade.rs +++ b/programs/marinade-beam/src/cpi_interface/marinade.rs @@ -1,3 +1,7 @@ +use crate::cpi_interface::program::Marinade; +use crate::cpi_interface::vault_authority_seed::VaultAuthoritySeed; +use crate::state::State; +use crate::{ExtractYield, OrderWithdrawal, Withdraw}; use anchor_lang::prelude::*; use marinade_cpi::cpi::{ accounts::{ @@ -22,42 +26,34 @@ pub fn deposit_stake_account(accounts: &crate::DepositStake, validator_index: u3 cpi_deposit_stake_account(cpi_ctx, validator_index) } -pub fn liquid_unstake(accounts: &crate::Withdraw, msol_lamports: u64) -> Result<()> { - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_ctx = CpiContext::new(cpi_program, accounts.into()); +pub fn liquid_unstake<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeLiquidUnstake<'info>, + msol_lamports: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; - cpi_liquid_unstake(cpi_ctx.with_signer(&[seeds]), msol_lamports) + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + cpi_liquid_unstake(cpi_ctx.with_signer(&[&seeds[..]]), msol_lamports) } -pub fn order_unstake(accounts: &crate::OrderWithdrawal, msol_lamports: u64) -> Result<()> { - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_accounts = MarinadeOrderUnstake { - state: accounts.marinade_state.to_account_info(), - msol_mint: accounts.msol_mint.to_account_info(), - burn_msol_from: accounts.msol_vault.to_account_info(), - burn_msol_authority: accounts.vault_authority.to_account_info(), - new_ticket_account: accounts.new_ticket_account.to_account_info(), - token_program: accounts.token_program.to_account_info(), - rent: accounts.rent.to_account_info(), - clock: accounts.clock.to_account_info(), - }; - let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); - - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; - cpi_order_unstake(cpi_ctx.with_signer(&[seeds]), msol_lamports) +pub fn order_unstake<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeOrderUnstake<'info>, + msol_lamports: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); + + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + cpi_order_unstake(cpi_ctx.with_signer(&[&seeds[..]]), msol_lamports) } pub fn claim_unstake_ticket(accounts: &crate::RedeemTicket) -> Result<()> { @@ -106,8 +102,8 @@ impl<'a> From<&crate::DepositStake<'a>> for MarinadeDepositStakeAccount<'a> { } } -impl<'a> From<&crate::Withdraw<'a>> for MarinadeLiquidUnstake<'a> { - fn from(accounts: &crate::Withdraw<'a>) -> Self { +impl<'a> From> for MarinadeLiquidUnstake<'a> { + fn from(accounts: Withdraw<'a>) -> Self { Self { state: accounts.marinade_state.to_account_info(), msol_mint: accounts.msol_mint.to_account_info(), @@ -123,8 +119,37 @@ impl<'a> From<&crate::Withdraw<'a>> for MarinadeLiquidUnstake<'a> { } } -impl<'a> From<&crate::OrderWithdrawal<'a>> for MarinadeOrderUnstake<'a> { - fn from(accounts: &crate::OrderWithdrawal<'a>) -> Self { +impl<'a> From<&Withdraw<'a>> for MarinadeLiquidUnstake<'a> { + fn from(accounts: &Withdraw<'a>) -> Self { + accounts.to_owned().into() + } +} + +impl<'a> From> for MarinadeLiquidUnstake<'a> { + fn from(accounts: ExtractYield<'a>) -> Self { + Self { + state: accounts.marinade_state.to_account_info(), + msol_mint: accounts.msol_mint.to_account_info(), + liq_pool_sol_leg_pda: accounts.liq_pool_sol_leg_pda.to_account_info(), + liq_pool_msol_leg: accounts.liq_pool_msol_leg.to_account_info(), + treasury_msol_account: accounts.treasury_msol_account.to_account_info(), + get_msol_from: accounts.msol_vault.to_account_info(), + get_msol_from_authority: accounts.vault_authority.to_account_info(), + transfer_sol_to: accounts.yield_account.to_account_info(), + system_program: accounts.system_program.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} + +impl<'a> From<&ExtractYield<'a>> for MarinadeLiquidUnstake<'a> { + fn from(accounts: &ExtractYield<'a>) -> Self { + accounts.to_owned().into() + } +} + +impl<'a> From<&OrderWithdrawal<'a>> for MarinadeOrderUnstake<'a> { + fn from(accounts: &OrderWithdrawal<'a>) -> Self { Self { state: accounts.marinade_state.to_account_info(), msol_mint: accounts.msol_mint.to_account_info(), diff --git a/programs/marinade-beam/src/cpi_interface/mod.rs b/programs/marinade-beam/src/cpi_interface/mod.rs index 1e3facc..3b0ecd6 100644 --- a/programs/marinade-beam/src/cpi_interface/mod.rs +++ b/programs/marinade-beam/src/cpi_interface/mod.rs @@ -1,2 +1,4 @@ pub mod marinade; +pub mod program; pub mod sunrise; +mod vault_authority_seed; diff --git a/programs/marinade-beam/src/cpi_interface/program.rs b/programs/marinade-beam/src/cpi_interface/program.rs new file mode 100644 index 0000000..ec627d3 --- /dev/null +++ b/programs/marinade-beam/src/cpi_interface/program.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::Id; + +#[derive(Clone)] +pub struct Marinade; + +impl Id for Marinade { + fn id() -> Pubkey { + marinade_cpi::ID + } +} diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index 2f495cb..f86f146 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::*; // TODO: Use actual CPI crate. +use crate::constants::STATE; use sunrise_core as sunrise_core_cpi; +use sunrise_core::cpi::accounts::ExtractYield; use sunrise_core_cpi::cpi::{ accounts::{BurnGsol, MintGsol}, - burn_gsol as cpi_burn_gsol, mint_gsol as cpi_mint_gsol, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, }; pub fn mint_gsol<'a>( @@ -109,3 +111,32 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { } } } + +pub fn extract_yield<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: ExtractYield<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_extract_yield( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + epoch_report: accounts.epoch_report.to_account_info(), + sysvar_clock: accounts.sysvar_clock.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} diff --git a/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs b/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs new file mode 100644 index 0000000..c90e1cf --- /dev/null +++ b/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs @@ -0,0 +1,27 @@ +use crate::state::State; +use anchor_lang::prelude::Account; +use anchor_lang::Key; + +pub struct VaultAuthoritySeed<'a> { + state_address: Vec, + vault_authority: &'a [u8], + bump: Vec, +} + +impl<'a> VaultAuthoritySeed<'a> { + pub fn new<'info>(state: &'a Account<'info, State>) -> Self { + let state_address = state.key().to_bytes().to_vec(); + let vault_authority = crate::constants::VAULT_AUTHORITY; + let bump = vec![state.vault_authority_bump]; + + VaultAuthoritySeed { + state_address, + vault_authority, + bump, + } + } + + pub fn as_slices(&self) -> [&[u8]; 3] { + [&self.state_address, self.vault_authority, &self.bump] + } +} diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 4def027..ee252ee 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -20,6 +20,7 @@ use system::accounts::{EpochReport, ProxyTicket}; use system::utils; // TODO: Use actual CPI crate. +use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; declare_id!("G9nMA5HvMa1HLXy1DBA3biH445Zxb2dkqsG4eDfcvgjm"); @@ -40,6 +41,7 @@ mod constants { #[program] pub mod marinade_beam { use super::*; + use crate::cpi_interface::marinade; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -104,9 +106,17 @@ pub mod marinade_beam { let msol_lamports = utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; + msg!("Liquid Unstake {} msol", msol_lamports); // CPI: Liquid unstake. - marinade_interface::liquid_unstake(ctx.accounts, msol_lamports)?; + let accounts = ctx.accounts.deref().into(); + marinade::liquid_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + msol_lamports, + )?; + msg!("Burn {} lamports", lamports); let bump = ctx.bumps.state; // CPI: Burn GSOL of the same proportion as the number of lamports withdrawn. sunrise_interface::burn_gsol( @@ -125,7 +135,13 @@ pub mod marinade_beam { let msol_lamports = utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; // CPI: Order unstake and receive a Marinade unstake ticket. - marinade_interface::order_unstake(ctx.accounts, msol_lamports)?; + let accounts = ctx.accounts.deref().into(); + marinade_interface::order_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + msol_lamports, + )?; // Create a program-owned account mapping the Marinade ticket to the beneficiary that ordered it. let ticket_account = &mut ctx.accounts.proxy_ticket_account; @@ -251,7 +267,7 @@ pub mod marinade_beam { &ctx.accounts.msol_vault, )?; ctx.accounts - .epoch_report_account + .epoch_report .add_extracted_yield(yield_lamports); let yield_msol = utils::calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports)?; @@ -259,7 +275,25 @@ pub mod marinade_beam { // TODO: Change to use delayed unstake so as not to incur fees. msg!("Withdrawing {} msol to yield account", yield_msol); // TODO: Legacy code uses liquid-unstake but leaves the note above. - // Move to delayed-unstakes here? + + let accounts = ctx.accounts.deref().into(); + marinade::liquid_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + yield_msol, + )?; + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::extract_yield( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + yield_msol, + )?; + Ok(()) } } @@ -371,10 +405,7 @@ pub struct Deposit<'info> { pub reserve_pda: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } @@ -446,17 +477,14 @@ pub struct DepositStake<'info> { pub stake_program: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } -#[derive(Accounts)] +#[derive(Accounts, Clone)] pub struct Withdraw<'info> { #[account( mut, @@ -513,10 +541,7 @@ pub struct Withdraw<'info> { pub instructions_sysvar: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade Program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } @@ -578,10 +603,7 @@ pub struct OrderWithdrawal<'info> { pub proxy_ticket_account: Box>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade Program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, @@ -621,10 +643,7 @@ pub struct RedeemTicket<'info> { )] pub vault_authority: UncheckedAccount<'info>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub system_program: Program<'info, System>, } @@ -675,7 +694,9 @@ pub struct Burn<'info> { pub struct ExtractYield<'info> { #[account( has_one = marinade_state, - has_one = sunrise_state + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump )] pub state: Box>, #[account( @@ -711,18 +732,31 @@ pub struct ExtractYield<'info> { /// CHECK: Matches the yield account key stored in the state. pub yield_account: UncheckedAccount<'info>, - #[account( - mut, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump = epoch_report_account.bump, - constraint = epoch_report_account.epoch == clock.epoch @ MarinadeBeamError::InvalidEpochReportAccount - )] - pub epoch_report_account: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub treasury_msol_account: UncheckedAccount<'info>, - pub clock: Sysvar<'info, Clock>, - //pub system_program: Program<'info, System>, - //pub token_program: Program<'info, Token>, - //pub marinade_program: Program<'info, MarinadeFinance>, + /// The epoch report account. This is updated with the latest extracted yield value. + /// It must be up to date with the current epoch. If not, run updateEpochReport before it. + /// CHECK: Address checked by CIP to the core Sunrise program. + #[account(mut)] + pub epoch_report: Box>, + + pub sysvar_clock: Sysvar<'info, Clock>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub marinade_program: Program<'info, Marinade>, + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, } #[derive(Accounts, Clone)] diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index e685bcd..4ac5b38 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -312,8 +312,7 @@ pub struct UpdateEpochReport<'info> { pub clock: Sysvar<'info, Clock>, } -#[derive(Accounts)] -#[instruction(amount_in_lamports: u64)] +#[derive(Accounts, Clone)] pub struct ExtractYield<'info> { pub state: Box>, From df33fdb140e6c76e9fada125a02549a6e6804e12 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Mon, 5 Feb 2024 09:53:27 +0100 Subject: [PATCH 2/8] Marinade SP: refactor calls to marinade liquid unstake - including a neater way to calculate the seeds. --- .../sdks/common/src/types/marinade_beam.ts | 306 ------------------ packages/sdks/marinade-sp/src/index.ts | 38 ++- .../src/functional/beams/marinade-sp.test.ts | 11 +- programs/marinade-beam/src/lib.rs | 151 +-------- programs/marinade-beam/src/system/accounts.rs | 45 --- programs/spl-beam/src/lib.rs | 2 +- 6 files changed, 35 insertions(+), 518 deletions(-) diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index aedc258..3772ded 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -646,123 +646,6 @@ export type MarinadeBeam = { ], "args": [] }, - { - "name": "initEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "sunriseState", - "isMut": false, - "isSigner": false - }, - { - "name": "marinadeState", - "isMut": false, - "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "updateAuthority", - "isMut": false, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "msolMint", - "isMut": true, - "isSigner": false - }, - { - "name": "msolVault", - "isMut": true, - "isSigner": false - }, - { - "name": "vaultAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "clock", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "extractedYield", - "type": "u64" - } - ] - }, - { - "name": "updateEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "sunriseState", - "isMut": false, - "isSigner": false - }, - { - "name": "marinadeState", - "isMut": false, - "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "msolMint", - "isMut": true, - "isSigner": false - }, - { - "name": "msolVault", - "isMut": true, - "isSigner": false - }, - { - "name": "vaultAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "clock", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "extractYield", "accounts": [ @@ -924,42 +807,6 @@ export type MarinadeBeam = { } ] } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": "publicKey" - }, - { - "name": "epoch", - "type": "u64" - }, - { - "name": "tickets", - "type": "u64" - }, - { - "name": "totalOrderedLamports", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" - } - ] - } } ], "types": [ @@ -1670,123 +1517,6 @@ export const IDL: MarinadeBeam = { ], "args": [] }, - { - "name": "initEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "sunriseState", - "isMut": false, - "isSigner": false - }, - { - "name": "marinadeState", - "isMut": false, - "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "updateAuthority", - "isMut": false, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "msolMint", - "isMut": true, - "isSigner": false - }, - { - "name": "msolVault", - "isMut": true, - "isSigner": false - }, - { - "name": "vaultAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "clock", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "extractedYield", - "type": "u64" - } - ] - }, - { - "name": "updateEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "sunriseState", - "isMut": false, - "isSigner": false - }, - { - "name": "marinadeState", - "isMut": false, - "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "msolMint", - "isMut": true, - "isSigner": false - }, - { - "name": "msolVault", - "isMut": true, - "isSigner": false - }, - { - "name": "vaultAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "clock", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "extractYield", "accounts": [ @@ -1948,42 +1678,6 @@ export const IDL: MarinadeBeam = { } ] } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": "publicKey" - }, - { - "name": "epoch", - "type": "u64" - }, - { - "name": "tickets", - "type": "u64" - }, - { - "name": "totalOrderedLamports", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" - } - ] - } } ], "types": [ diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index 88a1fb1..e79b53f 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -353,6 +353,7 @@ export class MarinadeClient extends BeamInterface< sunriseState: this.state.sunriseState, burner, gsolTokenAccount: burnGsolFrom, + vaultAuthority: this.vaultAuthority[0], systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, @@ -485,24 +486,29 @@ export class MarinadeClient extends BeamInterface< * Return a transaction to extract any yield from this beam into the yield account */ public async extractYield(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinade.state.solLeg(), + liqPoolMsolLeg: this.marinade.state.mSolLeg, + treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, + yieldAccount: this.sunrise.state.yieldAccount, + epochReport: this.sunrise.epochReport[0], + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarClock: SYSVAR_CLOCK_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + console.log(accounts); const instruction = await this.program.methods .extractYield() - .accounts({ - state: this.stateAddress, - marinadeState: this.state.proxyState, - sunriseState: this.state.sunriseState, - msolMint: this.marinade.state.mSolMint.address, - msolVault: this.marinade.beamMsolVault, - vaultAuthority: this.vaultAuthority[0], - liqPoolSolLegPda: await this.marinade.state.solLeg(), - liqPoolMsolLeg: this.marinade.state.mSolLeg, - treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, - sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, - sunriseProgram: this.sunrise.program.programId, - marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - }) + .accounts(accounts) .instruction(); return new Transaction().add(instruction); diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 1a05380..b40d45c 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -41,7 +41,7 @@ describe("Marinade stake pool beam", () => { const depositAmount = 10 * LAMPORTS_PER_SOL; const failedDepositAmount = 5 * LAMPORTS_PER_SOL; const liquidWithdrawalAmount = 5 * LAMPORTS_PER_SOL; - const delayedWithdrawalAmount = 5 * LAMPORTS_PER_SOL; + const delayedWithdrawalAmount = 1 * LAMPORTS_PER_SOL; const burnAmount = new BN(1 * LAMPORTS_PER_SOL); before("Set up the sunrise state", async () => { @@ -315,7 +315,14 @@ describe("Marinade stake pool beam", () => { ); }); - it("can extract yield into a stake account", async () => { + // TODO - we are going to restructure the epoch reports before enabling this + it.skip("can update the epoch report", async () => { + // fail + expect(false).to.equal(true); + }); + + // TODO - we are going to restructure the epoch reports before enabling this + it.skip("can extract yield into a stake account", async () => { // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index ee252ee..3d2fcf7 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -16,12 +16,13 @@ mod system; use cpi_interface::marinade as marinade_interface; use cpi_interface::sunrise as sunrise_interface; use state::{State, StateEntry}; -use system::accounts::{EpochReport, ProxyTicket}; +use system::accounts::ProxyTicket; use system::utils; // TODO: Use actual CPI crate. use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; +use sunrise_core::EpochReport; declare_id!("G9nMA5HvMa1HLXy1DBA3biH445Zxb2dkqsG4eDfcvgjm"); @@ -205,60 +206,6 @@ pub mod marinade_beam { Ok(()) } - pub fn init_epoch_report(ctx: Context, extracted_yield: u64) -> Result<()> { - let extractable_yield = utils::calculate_extractable_yield( - &ctx.accounts.sunrise_state, - &ctx.accounts.state, - &ctx.accounts.marinade_state, - &ctx.accounts.msol_vault, - )?; - let mut epoch_report = EpochReport { - state: ctx.accounts.state.key(), - epoch: ctx.accounts.clock.epoch, - tickets: 0, - total_ordered_lamports: 0, - extractable_yield, - extracted_yield: 0, // modified below with remarks - bump: ctx.bumps.epoch_report_account, - }; - // we have to trust that the extracted amount is accurate, - // as extracted yield is no longer managed by the program. - // This is why this instruction is only callable by the update authority - epoch_report.extracted_yield = extracted_yield; - - ctx.accounts.epoch_report_account.set_inner(epoch_report); - Ok(()) - } - - pub fn update_epoch_report(ctx: Context) -> Result<()> { - // we can update the epoch report if either - // a) the account is at the current epoch or - // b) the account is at the previous epoch and there are no open tickets - - let current_epoch = ctx.accounts.clock.epoch; - let is_previous_epoch = ctx.accounts.epoch_report_account.epoch == current_epoch - 1; - let is_current_epoch = ctx.accounts.epoch_report_account.epoch == current_epoch; - let is_previous_epoch_and_no_open_tickets = - is_previous_epoch && ctx.accounts.epoch_report_account.tickets == 0; - - require!( - is_current_epoch || is_previous_epoch_and_no_open_tickets, - MarinadeBeamError::RemainingUnclaimableTicketAmount - ); - - ctx.accounts.epoch_report_account.epoch = ctx.accounts.clock.epoch; - - let extractable_yield = utils::calculate_extractable_yield( - &ctx.accounts.sunrise_state, - &ctx.accounts.state, - &ctx.accounts.marinade_state, - &ctx.accounts.msol_vault, - )?; - msg!("Extractable yield: {}", extractable_yield); - ctx.accounts.epoch_report_account.extractable_yield = extractable_yield; - Ok(()) - } - pub fn extract_yield(ctx: Context) -> Result<()> { let yield_lamports = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, @@ -266,9 +213,6 @@ pub mod marinade_beam { &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, )?; - ctx.accounts - .epoch_report - .add_extracted_yield(yield_lamports); let yield_msol = utils::calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports)?; @@ -744,7 +688,7 @@ pub struct ExtractYield<'info> { /// The epoch report account. This is updated with the latest extracted yield value. /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - /// CHECK: Address checked by CIP to the core Sunrise program. + /// CHECK: Address checked by CPI to the core Sunrise program. #[account(mut)] pub epoch_report: Box>, @@ -759,95 +703,6 @@ pub struct ExtractYield<'info> { pub token_program: Program<'info, Token>, } -#[derive(Accounts, Clone)] -pub struct InitEpochReport<'info> { - #[account( - has_one = marinade_state, - has_one = sunrise_state, - has_one = update_authority - )] - pub state: Box>, - pub sunrise_state: Box>, - #[account(has_one = msol_mint)] - pub marinade_state: Box>, - - #[account(mut)] - pub payer: Signer<'info>, - pub update_authority: Signer<'info>, - - #[account( - init, - space = EpochReport::SPACE, - payer = payer, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump, - )] - pub epoch_report_account: Box>, - - #[account(mut)] - pub msol_mint: Box>, - #[account( - mut, - token::mint = msol_mint, - token::authority = vault_authority, - )] - pub msol_vault: Box>, - /// CHECK: Seeds of the MSOL vault authority. - #[account( - seeds = [ - state.key().as_ref(), - constants::VAULT_AUTHORITY - ], - bump = state.vault_authority_bump - )] - pub vault_authority: UncheckedAccount<'info>, - - pub clock: Sysvar<'info, Clock>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateEpochReport<'info> { - #[account( - has_one = marinade_state, - has_one = sunrise_state - )] - pub state: Box>, - pub sunrise_state: Box>, - #[account(has_one = msol_mint)] - pub marinade_state: Box>, - - #[account(mut)] - pub payer: Signer<'info>, - - #[account( - mut, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump = epoch_report_account.bump, - )] - pub epoch_report_account: Box>, - - #[account(mut)] - pub msol_mint: Box>, - #[account( - mut, - token::mint = msol_mint, - token::authority = vault_authority, - )] - pub msol_vault: Box>, - /// CHECK: Seeds of the MSOL vault authority. - #[account( - seeds = [ - state.key().as_ref(), - constants::VAULT_AUTHORITY - ], - bump = state.vault_authority_bump - )] - pub vault_authority: UncheckedAccount<'info>, - - pub clock: Sysvar<'info, Clock>, -} - #[error_code] pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] diff --git a/programs/marinade-beam/src/system/accounts.rs b/programs/marinade-beam/src/system/accounts.rs index 1b057d8..648b91c 100644 --- a/programs/marinade-beam/src/system/accounts.rs +++ b/programs/marinade-beam/src/system/accounts.rs @@ -1,4 +1,3 @@ -use crate::MarinadeBeamError; use anchor_lang::prelude::*; /// Maps a Marinade ticket account to a GSOL token holder @@ -11,47 +10,3 @@ pub struct ProxyTicket { impl ProxyTicket { pub const SPACE: usize = 32 + 32 + 32 + 8 /* DISCRIMINATOR */; } - -#[account] -pub struct EpochReport { - pub state: Pubkey, - pub epoch: u64, - pub tickets: u64, - pub total_ordered_lamports: u64, - pub extractable_yield: u64, - pub extracted_yield: u64, - pub bump: u8, -} - -impl EpochReport { - pub const SPACE: usize = 32 + 8 + 8 + 8 + 8 + 8 + 1 + 8 /* DISCRIMINATOR */ ; - - pub fn all_extractable_yield(&self) -> u64 { - self.extractable_yield - .checked_add(self.extracted_yield) - .unwrap() - } - - pub fn add_ticket(&mut self, ticket_amount_lamports: u64, clock: &Sysvar) -> Result<()> { - require_eq!( - self.epoch, - clock.epoch, - MarinadeBeamError::InvalidEpochReportAccount - ); - self.tickets = self.tickets.checked_add(1).unwrap(); - self.total_ordered_lamports = self - .total_ordered_lamports - .checked_add(ticket_amount_lamports) - .unwrap(); - Ok(()) - } - - pub fn add_extracted_yield(&mut self, extracted_yield: u64) { - self.extracted_yield = self.extracted_yield.checked_add(extracted_yield).unwrap(); - } - - pub fn update_report(&mut self, extractable_yield: u64, add_extracted_yield: u64) { - self.extractable_yield = extractable_yield; - self.add_extracted_yield(add_extracted_yield); - } -} diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index ec07df5..99be5c7 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -605,7 +605,7 @@ pub struct ExtractYield<'info> { /// The epoch report account. This is updated with the latest extracted yield value. /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - /// CHECK: Address checked by CIP to the core Sunrise program. + /// CHECK: Address checked by CPI to the core Sunrise program. #[account(mut)] pub epoch_report: UncheckedAccount<'info>, From b7898229cc4615c2edc69fc98004a7956079af3e Mon Sep 17 00:00:00 2001 From: dankelleher Date: Mon, 12 Feb 2024 11:29:55 +0100 Subject: [PATCH 3/8] Marinade SP: refactor update epoch report --- .../sdks/common/src/types/marinade_beam.ts | 152 +++++++-- .../sdks/common/src/types/marinade_lp_beam.ts | 12 +- packages/sdks/common/src/types/spl_beam.ts | 152 +++++++-- .../sdks/common/src/types/sunrise_core.ts | 294 +++++++++--------- packages/sdks/core/src/constants.ts | 2 - packages/sdks/core/src/index.ts | 38 +-- packages/sdks/core/src/state.ts | 32 ++ packages/sdks/marinade-lp/src/index.ts | 14 +- packages/sdks/marinade-sp/src/index.ts | 44 ++- packages/sdks/spl/src/index.ts | 21 +- .../tests/src/functional/beams/core.test.ts | 5 +- .../src/functional/beams/marinade-sp.test.ts | 36 ++- .../src/cpi_interface/sunrise.rs | 46 ++- programs/marinade-beam/src/lib.rs | 113 +++++-- programs/marinade-beam/src/system/utils.rs | 3 +- .../src/cpi_interface/sunrise.rs | 6 +- programs/marinade-lp-beam/src/lib.rs | 6 +- .../spl-beam/src/cpi_interface/sunrise.rs | 48 ++- programs/spl-beam/src/lib.rs | 86 ++++- .../src/instructions/burn_gsol.rs | 4 +- .../src/instructions/extract_yield.rs | 26 +- .../src/instructions/mint_gsol.rs | 4 +- .../src/instructions/register_state.rs | 3 +- .../src/instructions/update_epoch_report.rs | 74 ++--- programs/sunrise-core/src/lib.rs | 59 +--- programs/sunrise-core/src/seeds.rs | 1 - programs/sunrise-core/src/state.rs | 170 +++++++--- programs/sunrise-core/src/system.rs | 14 +- .../sunrise-core/tests/helpers/context.rs | 16 - .../tests/helpers/instructions.rs | 2 - 30 files changed, 959 insertions(+), 524 deletions(-) diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 3772ded..263278a 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -132,7 +132,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -251,7 +251,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -385,7 +385,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -469,7 +469,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -573,7 +573,7 @@ export type MarinadeBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -656,7 +656,7 @@ export type MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -704,15 +704,6 @@ export type MarinadeBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -745,6 +736,61 @@ export type MarinadeBeam = { } ], "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "msolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "msolVault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -1003,7 +1049,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1122,7 +1168,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1256,7 +1302,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1340,7 +1386,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1444,7 +1490,7 @@ export const IDL: MarinadeBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1527,7 +1573,7 @@ export const IDL: MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -1575,15 +1621,6 @@ export const IDL: MarinadeBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -1616,6 +1653,61 @@ export const IDL: MarinadeBeam = { } ], "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "msolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "msolVault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index e1459f6..accc1e3 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -138,7 +138,7 @@ export type MarinadeLpBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -277,7 +277,7 @@ export type MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -346,7 +346,7 @@ export type MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -615,7 +615,7 @@ export const IDL: MarinadeLpBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -754,7 +754,7 @@ export const IDL: MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -823,7 +823,7 @@ export const IDL: MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 8237497..7358ccc 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -150,7 +150,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -289,7 +289,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -398,7 +398,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -472,7 +472,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -591,7 +591,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -634,7 +634,7 @@ export type SplBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -643,9 +643,64 @@ export type SplBeam = { }, { "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "poolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sunriseProgram", "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, { "name": "stakePool", "isMut": true, @@ -704,15 +759,6 @@ export type SplBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -1003,7 +1049,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1142,7 +1188,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1251,7 +1297,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1325,7 +1371,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1444,7 +1490,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1487,7 +1533,7 @@ export const IDL: SplBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -1496,9 +1542,64 @@ export const IDL: SplBeam = { }, { "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "poolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sunriseProgram", "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, { "name": "stakePool", "isMut": true, @@ -1557,15 +1658,6 @@ export const IDL: SplBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index af05d75..cf657a7 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -18,11 +18,6 @@ export type SunriseCore = { "isMut": true, "isSigner": true }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false - }, { "name": "gsolMint", "isMut": false, @@ -208,7 +203,7 @@ export type SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -259,7 +254,7 @@ export type SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -349,13 +344,17 @@ export type SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "epochReport", - "isMut": true, - "isSigner": false + "name": "beam", + "isMut": false, + "isSigner": true, + "docs": [ + "The beam updating its epoch report.", + "This is verified in the handler to be a beam attached to this state." + ] }, { "name": "gsolMint", @@ -363,12 +362,17 @@ export type SunriseCore = { "isSigner": false }, { - "name": "clock", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "extractableYield", + "type": "u64" + } + ] }, { "name": "extractYield", @@ -378,8 +382,11 @@ export type SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, - "isSigner": false + "isMut": true, + "isSigner": false, + "docs": [ + "The core sunrise state - will have its epoch report updated." + ] }, { "name": "beam", @@ -390,15 +397,6 @@ export type SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -456,13 +454,6 @@ export type SunriseCore = { ], "type": "u8" }, - { - "name": "epochReportBump", - "docs": [ - "Bump of the eppch report PDA." - ], - "type": "u8" - }, { "name": "yieldAccount", "docs": [ @@ -492,34 +483,12 @@ export type SunriseCore = { "defined": "BeamDetails" } } - } - ] - } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "epoch", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "currentGsolSupply", - "type": "u64" }, { - "name": "bump", - "type": "u8" + "name": "epochReport", + "type": { + "defined": "EpochReport" + } } ] } @@ -529,7 +498,7 @@ export type SunriseCore = { { "name": "BeamDetails", "docs": [ - "Holds information about a registed beam." + "Holds information about a registered beam." ], "type": { "kind": "struct", @@ -653,6 +622,52 @@ export type SunriseCore = { } ] } + }, + { + "name": "EpochReport", + "type": { + "kind": "struct", + "fields": [ + { + "name": "currentGsolSupply", + "type": "u64" + }, + { + "name": "beamEpochDetails", + "docs": [ + "Holds [BeamEpochDetails] for all supported beams." + ], + "type": { + "vec": { + "defined": "BeamEpochDetails" + } + } + } + ] + } + }, + { + "name": "BeamEpochDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The most recent epoch that this beam has reported its extractable yield for" + ], + "type": "u64" + }, + { + "name": "extractableYield", + "type": "u64" + }, + { + "name": "extractedYield", + "type": "u64" + } + ] + } } ], "errors": [ @@ -703,31 +718,21 @@ export type SunriseCore = { }, { "code": 6009, - "name": "IncorrectBeamEpochReportCount", - "msg": "Incorrect amount of beam epoch reports" - }, - { - "code": 6010, - "name": "IncorrectBeamEpochReportEpoch", - "msg": "Incorrect epoch for beam epoch reports" - }, - { - "code": 6011, "name": "IncorrectBeamEpochReport", "msg": "Incorrect beam epoch report" }, { - "code": 6012, + "code": 6010, "name": "EpochReportAlreadyUpdated", "msg": "Epoch report already updated" }, { - "code": 6013, + "code": 6011, "name": "EpochReportNotUpToDate", "msg": "Epoch report not up to date" }, { - "code": 6014, + "code": 6012, "name": "Overflow", "msg": "Overflow" } @@ -754,11 +759,6 @@ export const IDL: SunriseCore = { "isMut": true, "isSigner": true }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false - }, { "name": "gsolMint", "isMut": false, @@ -944,7 +944,7 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -995,7 +995,7 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1085,13 +1085,17 @@ export const IDL: SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "epochReport", - "isMut": true, - "isSigner": false + "name": "beam", + "isMut": false, + "isSigner": true, + "docs": [ + "The beam updating its epoch report.", + "This is verified in the handler to be a beam attached to this state." + ] }, { "name": "gsolMint", @@ -1099,12 +1103,17 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "clock", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "extractableYield", + "type": "u64" + } + ] }, { "name": "extractYield", @@ -1114,8 +1123,11 @@ export const IDL: SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, - "isSigner": false + "isMut": true, + "isSigner": false, + "docs": [ + "The core sunrise state - will have its epoch report updated." + ] }, { "name": "beam", @@ -1126,15 +1138,6 @@ export const IDL: SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -1192,13 +1195,6 @@ export const IDL: SunriseCore = { ], "type": "u8" }, - { - "name": "epochReportBump", - "docs": [ - "Bump of the eppch report PDA." - ], - "type": "u8" - }, { "name": "yieldAccount", "docs": [ @@ -1228,34 +1224,12 @@ export const IDL: SunriseCore = { "defined": "BeamDetails" } } - } - ] - } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "epoch", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "currentGsolSupply", - "type": "u64" }, { - "name": "bump", - "type": "u8" + "name": "epochReport", + "type": { + "defined": "EpochReport" + } } ] } @@ -1265,7 +1239,7 @@ export const IDL: SunriseCore = { { "name": "BeamDetails", "docs": [ - "Holds information about a registed beam." + "Holds information about a registered beam." ], "type": { "kind": "struct", @@ -1389,6 +1363,52 @@ export const IDL: SunriseCore = { } ] } + }, + { + "name": "EpochReport", + "type": { + "kind": "struct", + "fields": [ + { + "name": "currentGsolSupply", + "type": "u64" + }, + { + "name": "beamEpochDetails", + "docs": [ + "Holds [BeamEpochDetails] for all supported beams." + ], + "type": { + "vec": { + "defined": "BeamEpochDetails" + } + } + } + ] + } + }, + { + "name": "BeamEpochDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The most recent epoch that this beam has reported its extractable yield for" + ], + "type": "u64" + }, + { + "name": "extractableYield", + "type": "u64" + }, + { + "name": "extractedYield", + "type": "u64" + } + ] + } } ], "errors": [ @@ -1439,31 +1459,21 @@ export const IDL: SunriseCore = { }, { "code": 6009, - "name": "IncorrectBeamEpochReportCount", - "msg": "Incorrect amount of beam epoch reports" - }, - { - "code": 6010, - "name": "IncorrectBeamEpochReportEpoch", - "msg": "Incorrect epoch for beam epoch reports" - }, - { - "code": 6011, "name": "IncorrectBeamEpochReport", "msg": "Incorrect beam epoch report" }, { - "code": 6012, + "code": 6010, "name": "EpochReportAlreadyUpdated", "msg": "Epoch report already updated" }, { - "code": 6013, + "code": 6011, "name": "EpochReportNotUpToDate", "msg": "Epoch report not up to date" }, { - "code": 6014, + "code": 6012, "name": "Overflow", "msg": "Overflow" } diff --git a/packages/sdks/core/src/constants.ts b/packages/sdks/core/src/constants.ts index 5f0273c..de93809 100644 --- a/packages/sdks/core/src/constants.ts +++ b/packages/sdks/core/src/constants.ts @@ -6,5 +6,3 @@ export const SUNRISE_PROGRAM_ID = new PublicKey( ); /** The constant seed of the GSOL mint authority PDA. */ export const GSOL_AUTHORITY_SEED = "gsol_mint_authority"; - -export const EPOCH_REPORT_SEED = "epoch_report"; diff --git a/packages/sdks/core/src/index.ts b/packages/sdks/core/src/index.ts index ca2f632..ff201c8 100644 --- a/packages/sdks/core/src/index.ts +++ b/packages/sdks/core/src/index.ts @@ -13,11 +13,7 @@ import { } from "@solana/spl-token"; import { sendAndConfirmChecked, SunriseCore } from "@sunrisestake/beams-common"; import { StateAccount } from "./state.js"; -import { - EPOCH_REPORT_SEED, - GSOL_AUTHORITY_SEED, - SUNRISE_PROGRAM_ID, -} from "./constants.js"; +import { GSOL_AUTHORITY_SEED, SUNRISE_PROGRAM_ID } from "./constants.js"; /** An instance of the Sunrise program that checks the validity of other * beams and regulates the minting and burning of GSOL. @@ -77,16 +73,11 @@ export class SunriseClient { programId, provider, ); - const epochReport = SunriseClient.deriveEpochReport( - state.publicKey, - programId, - )[0]; const register = await program.methods .registerState({ updateAuthority, yieldAccount, initialCapacity }) .accounts({ payer: provider.publicKey, state: state.publicKey, - epochReport, gsolMint, gsolMintAuthority: SunriseClient.deriveGsolMintAuthority( state.publicKey, @@ -186,7 +177,7 @@ export class SunriseClient { gsolMint: PublicKey; gsolMintAuthority: PublicKey; mintGsolTo: PublicKey; - instructionsSysvar: PublicKey; + sysvarInstructions: PublicKey; tokenProgram: PublicKey; } { return { @@ -196,7 +187,7 @@ export class SunriseClient { gsolMintAuthority: this.gsolMintAuthority[0], mintGsolTo: gsolTokenAccount ?? this.gsolAssociatedTokenAccount(tokenAccountOwner), - instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, }; } @@ -212,7 +203,7 @@ export class SunriseClient { gsolMint: PublicKey; burnGsolFromOwner: PublicKey; burnGsolFrom: PublicKey; - instructionsSysvar: PublicKey; + sysvarInstructions: PublicKey; tokenProgram: PublicKey; } { return { @@ -222,7 +213,7 @@ export class SunriseClient { burnGsolFromOwner: tokenAccountOwner, burnGsolFrom: gsolTokenAccount ?? this.gsolAssociatedTokenAccount(tokenAccountOwner), - instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, }; } @@ -237,16 +228,6 @@ export class SunriseClient { ); } - private static deriveEpochReport( - stateAddress: PublicKey, - programId = SUNRISE_PROGRAM_ID, - ): [PublicKey, number] { - return PublicKey.findProgramAddressSync( - [stateAddress.toBuffer(), Buffer.from(EPOCH_REPORT_SEED)], - programId, - ); - } - /** Get the address of the gsol mint authority. */ public get gsolMintAuthority(): [PublicKey, number] { return SunriseClient.deriveGsolMintAuthority( @@ -255,15 +236,6 @@ export class SunriseClient { ); } - /** Gets the address of the epoch report account */ - public get epochReport(): [PublicKey, number] { - console.log("this.stateAddress", this.stateAddress); - return SunriseClient.deriveEpochReport( - this.stateAddress, - this.program.programId, - ); - } - /** Derive the gsol ATA for a particular owner or the current provider key by default. */ public gsolAssociatedTokenAccount(owner?: PublicKey): PublicKey { return getAssociatedTokenAddressSync( diff --git a/packages/sdks/core/src/state.ts b/packages/sdks/core/src/state.ts index 4cc00c6..bc1a086 100644 --- a/packages/sdks/core/src/state.ts +++ b/packages/sdks/core/src/state.ts @@ -2,6 +2,15 @@ import { type PublicKey } from "@solana/web3.js"; import { type IdlAccounts, type BN } from "@coral-xyz/anchor"; import { SunriseCore } from "@sunrisestake/beams-common"; +export type EpochReport = { + currentGsolSupply: BN; + beamEpochDetails: Array<{ + epoch: BN; + extractableYield: BN; + extractedYield: BN; + }>; +}; + /** The deserialized state for the on-chain beam account.*/ export class StateAccount { public readonly address: PublicKey; @@ -11,6 +20,7 @@ export class StateAccount { public readonly gsolAuthBump: number; public readonly yieldAccount: PublicKey; public readonly beams: BeamDetails[]; + public readonly epochReport: EpochReport; private constructor( _address: PublicKey, @@ -23,6 +33,7 @@ export class StateAccount { this.gsolAuthBump = account.gsolMintAuthorityBump; this.yieldAccount = account.yieldAccount; this.beams = account.allocations; + this.epochReport = account.epochReport; } /** Create a new instance from an anchor-deserialized account. */ @@ -37,6 +48,8 @@ export class StateAccount { public pretty(): { [Property in keyof Omit]: Property extends "beams" ? Array + : Property extends "epochReport" + ? EpochReportPretty : string; } { return { @@ -47,6 +60,7 @@ export class StateAccount { gsolAuthBump: this.gsolAuthBump.toString(), yieldAccount: this.yieldAccount.toBase58(), beams: this.beams.map((beam) => printBeamDetails(beam)), + epochReport: printEpochReport(this.epochReport), }; } } @@ -68,3 +82,21 @@ const printBeamDetails = (raw: BeamDetails): BeamDetailsPretty => { drainingMode: raw.drainingMode.toString(), }; }; + +type EpochReportPretty = { + currentGsolSupply: string; + beamEpochDetails: Array<{ + epoch: string; + extractableYield: string; + extractedYield: string; + }>; +}; + +const printEpochReport = (raw: EpochReport): EpochReportPretty => ({ + currentGsolSupply: raw.currentGsolSupply.toString(), + beamEpochDetails: raw.beamEpochDetails.map((epoch) => ({ + epoch: epoch.epoch.toString(), + extractableYield: epoch.extractableYield.toString(), + extractedYield: epoch.extractedYield.toString(), + })), +}); diff --git a/packages/sdks/marinade-lp/src/index.ts b/packages/sdks/marinade-lp/src/index.ts index 68e955e..740d093 100644 --- a/packages/sdks/marinade-lp/src/index.ts +++ b/packages/sdks/marinade-lp/src/index.ts @@ -178,7 +178,7 @@ export class MarinadeLpClient extends BeamInterface< recipient?: PublicKey, ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -202,7 +202,7 @@ export class MarinadeLpClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, liqPoolMsolLegAuthority: @@ -230,7 +230,7 @@ export class MarinadeLpClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -256,7 +256,7 @@ export class MarinadeLpClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, }) @@ -287,7 +287,7 @@ export class MarinadeLpClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -304,7 +304,7 @@ export class MarinadeLpClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -320,7 +320,7 @@ export class MarinadeLpClient extends BeamInterface< // sunriseState: this.state.sunriseState, // systemProgram: SystemProgram.programId, // tokenProgram: TOKEN_PROGRAM_ID, - // instructionsSysvar, + // sysvarInstructions, // sunriseProgram: this.sunrise.program.programId, // }) // .instruction(); diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index e79b53f..89b5e21 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -184,7 +184,7 @@ export class MarinadeClient extends BeamInterface< recipient?: PublicKey, ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -208,7 +208,7 @@ export class MarinadeClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, liqPoolSolLegPda: await this.marinade.state.solLeg(), liqPoolMsolLeg: this.marinade.state.mSolLeg, liqPoolMsolLegAuthority: await this.marinade.state.mSolLegAuthority(), @@ -230,7 +230,7 @@ export class MarinadeClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -250,13 +250,12 @@ export class MarinadeClient extends BeamInterface< liqPoolSolLegPda: await this.marinade.state.solLeg(), liqPoolMsolLeg: this.marinade.state.mSolLeg, treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }; - console.log(accounts); const instruction = await this.program.methods .withdraw(amount) .accounts(accounts) @@ -277,7 +276,7 @@ export class MarinadeClient extends BeamInterface< proxyTicket: Keypair; }> { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -314,7 +313,7 @@ export class MarinadeClient extends BeamInterface< msolMint: this.marinade.state.mSolMint.address, msolVault: this.marinade.beamMsolVault, vaultAuthority: this.vaultAuthority[0], - instructionsSysvar, + sysvarInstructions, newTicketAccount: marinadeTicket.publicKey, proxyTicketAccount: sunriseTicket.publicKey, sunriseProgram: this.sunrise.program.programId, @@ -339,7 +338,7 @@ export class MarinadeClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -357,7 +356,7 @@ export class MarinadeClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -395,7 +394,7 @@ export class MarinadeClient extends BeamInterface< recipient?: PublicKey, ): Promise { const stakeOwner = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, stakeOwner); const transaction = new Transaction(); @@ -440,7 +439,7 @@ export class MarinadeClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, validatorList: info.validatorSystem.validatorList.account, stakeList: info.stakeSystem.stakeList.account, duplicationFlag: @@ -482,6 +481,27 @@ export class MarinadeClient extends BeamInterface< return Utils.deriveStateAddress(PID, sunriseState); }; + // Update this beam's extractable yield in the core state's epoch report + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + gsolMint: this.sunrise.state.gsolMint, + vaultAuthority: this.vaultAuthority[0], + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + /** * Return a transaction to extract any yield from this beam into the yield account */ @@ -497,7 +517,6 @@ export class MarinadeClient extends BeamInterface< liqPoolMsolLeg: this.marinade.state.mSolLeg, treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, yieldAccount: this.sunrise.state.yieldAccount, - epochReport: this.sunrise.epochReport[0], sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, sysvarClock: SYSVAR_CLOCK_PUBKEY, sunriseProgram: this.sunrise.program.programId, @@ -505,7 +524,6 @@ export class MarinadeClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }; - console.log(accounts); const instruction = await this.program.methods .extractYield() .accounts(accounts) diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index e230891..5b080ec 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -191,7 +191,7 @@ export class SplClient extends BeamInterface { ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -218,7 +218,7 @@ export class SplClient extends BeamInterface { managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -237,7 +237,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -262,7 +262,7 @@ export class SplClient extends BeamInterface { sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, nativeStakeProgram: StakeProgram.programId, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -281,7 +281,7 @@ export class SplClient extends BeamInterface { recipient?: PublicKey, ): Promise { const stakeOwner = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, stakeOwner); const stakeAccountInfo = await getParsedStakeAccountInfo( @@ -324,7 +324,7 @@ export class SplClient extends BeamInterface { nativeStakeProgram: StakeProgram.programId, gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -344,7 +344,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -371,7 +371,7 @@ export class SplClient extends BeamInterface { sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, nativeStakeProgram: StakeProgram.programId, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -401,7 +401,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -419,7 +419,7 @@ export class SplClient extends BeamInterface { systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -449,7 +449,6 @@ export class SplClient extends BeamInterface { validatorStakeList: this.spl.stakePoolState.validatorList, stakeAccountToSplit: this.spl.stakePoolState.reserveStake, managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, - epochReport: this.sunrise.epochReport[0], sysvarClock: SYSVAR_CLOCK_PUBKEY, nativeStakeProgram: StakeProgram.programId, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, diff --git a/packages/tests/src/functional/beams/core.test.ts b/packages/tests/src/functional/beams/core.test.ts index 09a892e..e52a5d2 100644 --- a/packages/tests/src/functional/beams/core.test.ts +++ b/packages/tests/src/functional/beams/core.test.ts @@ -13,6 +13,7 @@ import { provider } from "../setup.js"; import { expect } from "chai"; const BEAM_DETAILS_LEN: number = 170; +const EPOCH_REPORT_LEN: number = 24; describe("Sunrise core", () => { let gsolMint: PublicKey; @@ -137,7 +138,9 @@ describe("Sunrise core", () => { const finalLen = await provider.connection .getAccountInfo(client.stateAddress) .then((info) => info!.data.length); - expect(finalLen).to.equal(initialLen + 5 * BEAM_DETAILS_LEN); + expect(finalLen).to.equal( + initialLen + 5 * BEAM_DETAILS_LEN + 5 * EPOCH_REPORT_LEN, + ); await client.refresh(); for (const beam of client.state.pretty().beams.slice(15, 20)) { diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index b40d45c..00d52cb 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -33,9 +33,9 @@ describe("Marinade stake pool beam", () => { let beamClient: MarinadeClient; let vaultMsolBalance: BN; let stakerGsolBalance: BN = new BN(0); + let extractableYield: BN; let sunriseStateAddress: PublicKey; - let sunriseDelayedTicket: PublicKey; const depositAmount = 10 * LAMPORTS_PER_SOL; @@ -315,14 +315,26 @@ describe("Marinade stake pool beam", () => { ); }); - // TODO - we are going to restructure the epoch reports before enabling this - it.skip("can update the epoch report", async () => { - // fail - expect(false).to.equal(true); + it("can update the epoch report", async () => { + // we burned `burnAmount` gsol, so we should be able to extract `burnAmount` - estimated fee + const expectedFee = burnAmount.toNumber() * 0.003; + extractableYield = burnAmount.subn(expectedFee); + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(extractableYield.toNumber()); }); - // TODO - we are going to restructure the epoch reports before enabling this - it.skip("can extract yield into a stake account", async () => { + it("can extract yield into a stake account", async () => { // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. @@ -333,17 +345,11 @@ describe("Marinade stake pool beam", () => { await beamClient.extractYield(), ); - // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account - const expectedFee = new BN(0); // TODO - const expectedExtractedYield = burnAmount.sub(expectedFee); - await expectSolBalance( beamClient.provider, beamClient.sunrise.state.yieldAccount, - expectedExtractedYield, - // // the calculation appears to be slightly inaccurate at present, but in our favour, - // // so we can leave this as a low priority TODO to improve the accuracy - // 3000, + extractableYield, // calculated in the previous test + 1, ); }); }); diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index f86f146..27c1b91 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -1,11 +1,10 @@ -use anchor_lang::prelude::*; -// TODO: Use actual CPI crate. use crate::constants::STATE; +use anchor_lang::prelude::*; use sunrise_core as sunrise_core_cpi; -use sunrise_core::cpi::accounts::ExtractYield; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, MintGsol}, + accounts::{BurnGsol, ExtractYield, MintGsol, UpdateEpochReport}, burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -50,7 +49,7 @@ impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -64,7 +63,7 @@ impl<'a> From<&crate::DepositStake<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -78,7 +77,7 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -92,7 +91,7 @@ impl<'a> From<&crate::OrderWithdrawal<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -106,7 +105,7 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -134,9 +133,36 @@ impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), - epoch_report: accounts.epoch_report.to_account_info(), sysvar_clock: accounts.sysvar_clock.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } } + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 3d2fcf7..dda4bdb 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -19,10 +19,8 @@ use state::{State, StateEntry}; use system::accounts::ProxyTicket; use system::utils; -// TODO: Use actual CPI crate. use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; -use sunrise_core::EpochReport; declare_id!("G9nMA5HvMa1HLXy1DBA3biH445Zxb2dkqsG4eDfcvgjm"); @@ -31,8 +29,6 @@ mod constants { pub const VAULT_AUTHORITY: &[u8] = b"vault-authority"; /// Seed of this program's state address. pub const STATE: &[u8] = b"sunrise-marinade"; - /// Seed of the epoch report account. - pub const EPOCH_REPORT: &[u8] = b"marinade-epoch-report"; // TODO: RECOVERED_MARGIN is needed because, for some reason, the claim tickets have a couple of lamports less than they should, // probably due to a rounding error converting to and from marinade. // Figure this out and then remove this margin @@ -216,10 +212,9 @@ pub mod marinade_beam { let yield_msol = utils::calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports)?; - // TODO: Change to use delayed unstake so as not to incur fees. - msg!("Withdrawing {} msol to yield account", yield_msol); - // TODO: Legacy code uses liquid-unstake but leaves the note above. + let yield_account_balance_before = ctx.accounts.yield_account.lamports(); + // TODO: Change to use delayed unstake so as not to incur fees. let accounts = ctx.accounts.deref().into(); marinade::liquid_unstake( &ctx.accounts.marinade_program, @@ -228,6 +223,12 @@ pub mod marinade_beam { yield_msol, )?; + let yield_account_balance_after = ctx.accounts.yield_account.lamports(); + let withdrawn_lamports = + yield_account_balance_after.saturating_sub(yield_account_balance_before); + + msg!("Withdrawn {} lamports to yield account", withdrawn_lamports); + // CPI: update the epoch report with the extracted yield. let state_bump = ctx.bumps.state; sunrise_interface::extract_yield( @@ -235,7 +236,32 @@ pub mod marinade_beam { ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), state_bump, - yield_msol, + withdrawn_lamports, + )?; + + Ok(()) + } + + pub fn update_epoch_report(ctx: Context) -> Result<()> { + let yield_lamports = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.marinade_state, + &ctx.accounts.msol_vault, + )?; + + // Reduce by fee + // TODO can we do better than an estimate? + let extractable_lamports = (yield_lamports as f64) * 0.997; // estimated 0.3% unstake fee + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + extractable_lamports as u64, )?; Ok(()) @@ -332,7 +358,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -404,7 +430,7 @@ pub struct DepositStake<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -482,7 +508,7 @@ pub struct Withdraw<'info> { pub treasury_msol_account: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub marinade_program: Program<'info, Marinade>, @@ -534,7 +560,7 @@ pub struct OrderWithdrawal<'info> { pub vault_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, /// CHECK: Checked by Marinade CPI. #[account(mut)] @@ -629,7 +655,7 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } @@ -637,14 +663,15 @@ pub struct Burn<'info> { #[derive(Accounts, Clone)] pub struct ExtractYield<'info> { #[account( - has_one = marinade_state, - has_one = sunrise_state, - seeds = [constants::STATE, sunrise_state.key().as_ref()], - bump + has_one = marinade_state, + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump )] pub state: Box>, #[account( - has_one = yield_account + mut, // Update the extracted yield on the state's epoch report. + has_one = yield_account )] pub sunrise_state: Box>, #[account(mut)] @@ -686,12 +713,6 @@ pub struct ExtractYield<'info> { /// CHECK: Checked by Marinade CPI. pub treasury_msol_account: UncheckedAccount<'info>, - /// The epoch report account. This is updated with the latest extracted yield value. - /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - /// CHECK: Address checked by CPI to the core Sunrise program. - #[account(mut)] - pub epoch_report: Box>, - pub sysvar_clock: Sysvar<'info, Clock>, /// CHECK: Checked by Sunrise CPI. @@ -703,6 +724,50 @@ pub struct ExtractYield<'info> { pub token_program: Program<'info, Token>, } +#[derive(Accounts, Clone)] +pub struct UpdateEpochReport<'info> { + #[account( + has_one = marinade_state, + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + #[account( + mut, // Update the extractable yield on the state's epoch report. + )] + pub sunrise_state: Box>, + #[account( + has_one = msol_mint, + )] + pub marinade_state: Box>, + + pub msol_mint: Box>, + #[account( + token::mint = msol_mint, + token::authority = vault_authority, + )] + pub msol_vault: Box>, + /// CHECK: Seeds of the MSOL vault authority. + #[account( + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + pub vault_authority: UncheckedAccount<'info>, + + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, +} + #[error_code] pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] diff --git a/programs/marinade-beam/src/system/utils.rs b/programs/marinade-beam/src/system/utils.rs index 5f7caab..aa3a039 100644 --- a/programs/marinade-beam/src/system/utils.rs +++ b/programs/marinade-beam/src/system/utils.rs @@ -14,8 +14,7 @@ pub fn calculate_extractable_yield( marinade_state: &MarinadeState, msol_vault: &TokenAccount, ) -> Result { - let staked_value = - super::utils::calc_lamports_from_msol_amount(marinade_state, msol_vault.amount)?; + let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount)?; let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index 11ca030..a234ed8 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -48,7 +48,7 @@ impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -62,7 +62,7 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -76,7 +76,7 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index e24e3a9..62a5318 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -214,7 +214,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -298,7 +298,7 @@ pub struct Withdraw<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, #[account(address = marinade_cpi::ID)] @@ -333,7 +333,7 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index 29d32c2..fbba535 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -2,8 +2,9 @@ use crate::seeds::*; use anchor_lang::prelude::*; use sunrise_core as sunrise_core_cpi; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, ExtractYield, MintGsol}, + accounts::{BurnGsol, ExtractYield, MintGsol, UpdateEpochReport}, burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -37,7 +38,7 @@ impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -50,7 +51,7 @@ impl<'a> From<&crate::DepositStake<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -87,7 +88,7 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -101,7 +102,7 @@ impl<'a> From<&crate::WithdrawStake<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -115,7 +116,7 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -149,9 +150,42 @@ impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), - epoch_report: accounts.epoch_report.to_account_info(), sysvar_clock: accounts.sysvar_clock.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } } + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + stake_pool: Pubkey, + state_bump: u8, + extractable_yield: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [ + STATE, + sunrise_key.as_ref(), + stake_pool.as_ref(), + &[state_bump], + ]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + extractable_yield, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index 99be5c7..54722a9 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -166,6 +166,29 @@ pub mod spl_beam { Err(SplBeamError::Unimplemented.into()) } + pub fn update_epoch_report(ctx: Context) -> Result<()> { + // Calculate how much yield can be extracted from the pool. + let extractable_yield = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.stake_pool, + &ctx.accounts.pool_token_vault, + )?; + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + ctx.accounts.stake_pool.key(), + state_bump, + extractable_yield, + )?; + + Ok(()) + } + pub fn extract_yield(ctx: Context) -> Result<()> { // Calculate how much yield can be extracted from the pool. let extractable_yield = utils::calculate_extractable_yield( @@ -301,7 +324,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by CPI to Sunrise. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -379,7 +402,7 @@ pub struct DepositStake<'info> { /// CHECK: Checked by CPI to Sunrise. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -445,7 +468,7 @@ pub struct Withdraw<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -520,7 +543,7 @@ pub struct WithdrawStake<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -539,10 +562,14 @@ pub struct ExtractYield<'info> { )] pub state: Box>, #[account( + mut, // Update the extracted yield on the state's epoch report. has_one = yield_account )] pub sunrise_state: Box>, - #[account(mut)] + #[account( + mut, + has_one = pool_mint + )] pub stake_pool: Box>, #[account(mut)] @@ -603,12 +630,6 @@ pub struct ExtractYield<'info> { /// CHECK: Checked by CPI to SPL StakePool Program. pub manager_fee_account: UncheckedAccount<'info>, - /// The epoch report account. This is updated with the latest extracted yield value. - /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - /// CHECK: Address checked by CPI to the core Sunrise program. - #[account(mut)] - pub epoch_report: UncheckedAccount<'info>, - pub sysvar_clock: Sysvar<'info, Clock>, pub native_stake_program: Program<'info, NativeStakeProgram>, /// CHECK: Checked by CPI to SPL Stake program. @@ -623,6 +644,47 @@ pub struct ExtractYield<'info> { pub token_program: Program<'info, Token>, } +#[derive(Accounts, Clone)] +pub struct UpdateEpochReport<'info> { + #[account( + has_one = stake_pool, + has_one = sunrise_state, + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + bump + )] + pub state: Box>, + #[account( + mut, // Update the extractable yield on the state's epoch report. + )] + pub sunrise_state: Box>, + pub stake_pool: Box>, + pub pool_mint: Box>, + + #[account( + seeds = [ + state.key().as_ref(), + VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + #[account( + token::mint = pool_mint, + token::authority = vault_authority + )] + pub pool_token_vault: Box>, + + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + /// CHECK: Checked by CPI to Sunrise. + pub sysvar_instructions: UncheckedAccount<'info>, +} + #[derive(Accounts)] pub struct Burn<'info> { #[account( @@ -645,7 +707,7 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index beaae65..39286e1 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -7,8 +7,8 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { // Check that the requesting program is valid. let cpi_program = - utils::get_cpi_program_id(&ctx.accounts.instructions_sysvar.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; let details = state .get_mut_beam_details(&ctx.accounts.beam.key()) diff --git a/programs/sunrise-core/src/instructions/extract_yield.rs b/programs/sunrise-core/src/instructions/extract_yield.rs index e07670e..e802a53 100644 --- a/programs/sunrise-core/src/instructions/extract_yield.rs +++ b/programs/sunrise-core/src/instructions/extract_yield.rs @@ -7,26 +7,32 @@ use crate::{system, utils, BeamError, ExtractYield}; /// It does not send funds to the yield account (that is done by the beam program itself) /// It only updates the extracted yield on the epoch report. pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { - let state = &ctx.accounts.state; - let epoch_report = &mut ctx.accounts.epoch_report; + let state = &mut ctx.accounts.state; let current_epoch = ctx.accounts.sysvar_clock.epoch; // Check that the executing program is valid. let cpi_program = utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + let beam_idx = system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + let beam_epoch_details = &state.epoch_report.beam_epoch_details[beam_idx]; - // The epoch report must be already updated for this epoch + // The epoch report must be already updated for this epoch and beam require!( - epoch_report.epoch == current_epoch, + beam_epoch_details.epoch == current_epoch, BeamError::EpochReportNotUpToDate ); - // Update the extracted yield on the report - epoch_report - .extracted_yield - .checked_add(amount_in_lamports) - .ok_or(BeamError::Overflow)?; + msg!( + "Registering extracted yield of {} lamports for beam {}", + amount_in_lamports, + beam_idx + ); + + // Update the extracted yield on the epoch report for the beam, + // if the epoch report has already been updated for this epoch and beam (TODO is this check strictly necessary?) + state + .epoch_report + .extract_yield_for_beam(beam_idx, amount_in_lamports, current_epoch)?; Ok(()) } diff --git a/programs/sunrise-core/src/instructions/mint_gsol.rs b/programs/sunrise-core/src/instructions/mint_gsol.rs index 21d98d5..466a104 100644 --- a/programs/sunrise-core/src/instructions/mint_gsol.rs +++ b/programs/sunrise-core/src/instructions/mint_gsol.rs @@ -8,8 +8,8 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { // Check that the executing program is valid. let cpi_program = - utils::get_cpi_program_id(&ctx.accounts.instructions_sysvar.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; let pre_supply = state.pre_supply; let effective_supply = gsol_mint.supply.checked_sub(pre_supply).unwrap(); diff --git a/programs/sunrise-core/src/instructions/register_state.rs b/programs/sunrise-core/src/instructions/register_state.rs index b0ee1e8..82f0bba 100644 --- a/programs/sunrise-core/src/instructions/register_state.rs +++ b/programs/sunrise-core/src/instructions/register_state.rs @@ -5,10 +5,9 @@ pub fn handler(ctx: Context, input: RegisterStateInput) -> Result let state_account = &mut ctx.accounts.state; let auth_bump = ctx.bumps.gsol_mint_authority; - let epoch_report_bump = ctx.bumps.epoch_report; let mint_key = ctx.accounts.gsol_mint.key(); let mint_supply = ctx.accounts.gsol_mint.supply; - state_account.register(input, auth_bump, &mint_key, mint_supply, epoch_report_bump); + state_account.register(input, auth_bump, &mint_key, mint_supply)?; Ok(()) } diff --git a/programs/sunrise-core/src/instructions/update_epoch_report.rs b/programs/sunrise-core/src/instructions/update_epoch_report.rs index 18d6dc2..d5e0444 100644 --- a/programs/sunrise-core/src/instructions/update_epoch_report.rs +++ b/programs/sunrise-core/src/instructions/update_epoch_report.rs @@ -1,55 +1,35 @@ use anchor_lang::prelude::*; -use crate::{BeamError, EpochReport, UpdateEpochReport}; - -pub fn handler<'c: 'info, 'info>(ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>) -> Result<()> { - let state = &ctx.accounts.state; - let epoch_report = &mut ctx.accounts.epoch_report; - let current_epoch = ctx.accounts.clock.epoch; - - // The epoch report must not be already updated for this epoch - require!( - epoch_report.epoch < current_epoch, - BeamError::EpochReportAlreadyUpdated - ); - - // Remaining accounts must all be beam-owned Epoch Report accounts. - // The count must equal the number of beams in the state. - let beam_count = state.beam_count(); - let beam_owned_epoch_report_count = ctx.remaining_accounts.len(); - require!( - beam_count == beam_owned_epoch_report_count, - BeamError::IncorrectBeamEpochReportCount +use crate::{system, utils, UpdateEpochReport}; + +/// Called by a beam via CPI - to update its epoch report. +/// Once all beams have called in via CPI, then the epoch report is considered updated for the given epoch +/// However, a beam is allowed to call in multiple times to update its epoch report for a given epoch. +pub fn handler<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>, + extractable_yield: u64, +) -> Result<()> { + let state = &mut ctx.accounts.state; + let current_epoch = Clock::get().unwrap().epoch; + + // Check that the executing program is valid. + let cpi_program = + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + let beam_idx = system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + + msg!( + "Updating extractable yield for beam {} to {}", + beam_idx, + extractable_yield ); - // Iterate through the remaining accounts, convert them to Epoch Reports - for (index, beam_owned_epoch_report_account) in ctx.remaining_accounts.iter().enumerate() { - // The epoch of each beam-owned epoch report must match the current epoch - let beam_owned_epoch_report: Account = - Account::try_from(beam_owned_epoch_report_account)?; - require!( - beam_owned_epoch_report.epoch == current_epoch, - BeamError::IncorrectBeamEpochReportEpoch - ); - - // Check that they are the expected beam account - require_keys_eq!( - *beam_owned_epoch_report_account.key, - state.allocations[index].key, - BeamError::IncorrectBeamEpochReport - ); - - // Add the totals to the epoch report - epoch_report - .extractable_yield - .checked_add(beam_owned_epoch_report.extractable_yield) - .ok_or(BeamError::Overflow)?; - } + // Update the epoch report with the current extractable yield + state + .epoch_report + .update_extractable_yield_and_epoch_for_beam(beam_idx, current_epoch, extractable_yield); - // Update the epoch to the current one - epoch_report.epoch = current_epoch; - // Set the current gsol supply - epoch_report.current_gsol_supply = ctx.accounts.gsol_mint.supply; + // Update the current gsol supply + state.epoch_report.current_gsol_supply = ctx.accounts.gsol_mint.supply; Ok(()) } diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index 4ac5b38..a724a92 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -89,8 +89,9 @@ pub mod sunrise_core { /// Updates the Epoch Report Account, which stores the amount of yield extracted or extractable over time pub fn update_epoch_report<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>, + extractable_yield: u64, ) -> Result<()> { - update_epoch_report::handler(ctx) + update_epoch_report::handler(ctx, extractable_yield) } /// CPI request from a beam program to extract yield from Sunrise @@ -112,18 +113,6 @@ pub struct RegisterState<'info> { )] pub state: Account<'info, State>, - #[account( - init, - payer = payer, - space = EpochReport::SIZE, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump - )] - pub epoch_report: Account<'info, EpochReport>, - pub gsol_mint: Account<'info, Mint>, /// CHECK: Valid PDA seeds. @@ -200,7 +189,7 @@ pub struct BurnGsol<'info> { /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -233,7 +222,7 @@ pub struct MintGsol<'info> { /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -291,47 +280,35 @@ pub struct ResizeAllocations<'info> { } #[derive(Accounts)] +#[instruction(extractable_yield: u64)] pub struct UpdateEpochReport<'info> { #[account( + mut, has_one = gsol_mint, )] pub state: Box>, - #[account( - mut, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump = state.epoch_report_bump - )] - pub epoch_report: Account<'info, EpochReport>, + /// The beam updating its epoch report. + /// This is verified in the handler to be a beam attached to this state. + pub beam: Signer<'info>, pub gsol_mint: Box>, - pub clock: Sysvar<'info, Clock>, + /// CHECK: Verified Instructions Sysvar. + #[account(address = sysvar::instructions::ID)] + pub sysvar_instructions: UncheckedAccount<'info>, } #[derive(Accounts, Clone)] pub struct ExtractYield<'info> { + /// The core sunrise state - will have its epoch report updated. + #[account(mut)] pub state: Box>, /// The beam contributing the extracted yield. /// This is verified in the handler to be a beam attached to this state. pub beam: Signer<'info>, - /// The epoch report account. This is updated with the latest extracted yield value. - /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - #[account( - mut, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump = state.epoch_report_bump - )] - pub epoch_report: Account<'info, EpochReport>, - pub sysvar_clock: Sysvar<'info, Clock>, /// CHECK: Verified Instructions Sysvar. @@ -377,14 +354,6 @@ pub enum BeamError { #[msg("Can't remove a beam with a non-zero allocation")] NonZeroAllocation, - /// Thrown if the epoch report is updated with an incorrect amount of beam epoch reports - #[msg("Incorrect amount of beam epoch reports")] - IncorrectBeamEpochReportCount, - - /// Thrown if the epoch report is updated with beam epoch reports from the wrong epoch - #[msg("Incorrect epoch for beam epoch reports")] - IncorrectBeamEpochReportEpoch, - /// Thrown if the epoch report is updated by an incorrect beam epoch report #[msg("Incorrect beam epoch report")] IncorrectBeamEpochReport, diff --git a/programs/sunrise-core/src/seeds.rs b/programs/sunrise-core/src/seeds.rs index 22bf191..1721149 100644 --- a/programs/sunrise-core/src/seeds.rs +++ b/programs/sunrise-core/src/seeds.rs @@ -1,2 +1 @@ pub const GSOL_AUTHORITY: &[u8] = b"gsol_mint_authority"; -pub const EPOCH_REPORT: &[u8] = b"epoch_report"; diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 60269a8..d4b7704 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -18,9 +18,6 @@ pub struct State { /// Bump of the gSol mint authority PDA. pub gsol_mint_authority_bump: u8, - /// Bump of the eppch report PDA. - pub epoch_report_bump: u8, - /// The Sunrise yield account. pub yield_account: Pubkey, @@ -29,9 +26,11 @@ pub struct State { /// Holds [BeamDetails] for all supported beams. pub allocations: Vec, + + pub epoch_report: EpochReport, } -/// Holds information about a registed beam. +/// Holds information about a registered beam. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct BeamDetails { /// The beam's signer for mint and burn requests. @@ -77,14 +76,16 @@ impl State { 32 + // gsol_mint 8 + // pre_supply 1 + // gsol_mint_authority_bump - 1 + // epoch_report_bump 32 + // yield_account 128 + // reserved_space - 4; // vec size + 4; // allocations vec size + // Does not include epoch_report min size (included in size() and size /// Calculate the borsh-serialized size of a state with `beam_count` number of beams. pub fn size(beam_count: usize) -> usize { - Self::SIZE_WITH_ZERO_BEAMS + (BeamDetails::SIZE * beam_count) // allocations vec + Self::SIZE_WITH_ZERO_BEAMS + + (BeamDetails::SIZE * beam_count) +// allocations vec + EpochReport::size(beam_count) // epoch_reports } /// Calculate the size of a state account. @@ -99,8 +100,7 @@ impl State { gsol_mint_auth_bump: u8, gsol_mint: &Pubkey, gsol_mint_supply: u64, - epoch_report_bump: u8, - ) { + ) -> Result<()> { self.update_authority = input.update_authority; self.yield_account = input.yield_account; self.gsol_mint = *gsol_mint; @@ -110,11 +110,7 @@ impl State { self.pre_supply = gsol_mint_supply; self.gsol_mint_authority_bump = gsol_mint_auth_bump; - // The epoch report bump is used to derive the PDA of this state's epoch report account. - // Storing it here reduces the cost of subsequent update operations. - self.epoch_report_bump = epoch_report_bump; - - // We fill up the vec because deserialization of an empty vec would result + // We fill up the vecs because deserialization of an empty vec would result // in the active capacity being lost. i.e a vector with capacity 10 but length // 4 would be deserialized as having both length and capacity of 4. // @@ -125,6 +121,11 @@ impl State { // * Add a beam by finding and replacing a default BeamDetails struct. // * Remove a beam by replacing it with a default BeamDetails struct. self.allocations = vec![BeamDetails::default(); input.initial_capacity as usize]; + + let current_epoch = Clock::get()?.epoch; + self.epoch_report = EpochReport::new(input.initial_capacity as usize, current_epoch); + + Ok(()) } /// Update the fields of a [State] object. @@ -206,6 +207,10 @@ impl State { pub fn contains_beam(&self, key: &Pubkey) -> bool { self.get_beam_details(key).is_some() } + + pub fn find_beam_index(&self, key: &Pubkey) -> Option { + self.allocations.iter().position(|x| x.key == *key) + } } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] @@ -232,21 +237,110 @@ pub struct AllocationUpdate { pub new_allocation: u8, } -#[account] +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct EpochReport { + pub current_gsol_supply: u64, + + /// Holds [BeamEpochDetails] for all supported beams. + pub beam_epoch_details: Vec, +} +impl EpochReport { + pub const SIZE_WITH_ZERO_BEAMS: usize = 8 + // current_gsol_supply + 4; // vec size + + pub fn new(beam_count: usize, current_epoch: u64) -> EpochReport { + EpochReport { + current_gsol_supply: 0, + // see details in the State::register method + beam_epoch_details: vec![BeamEpochDetails::new(current_epoch); beam_count], + } + } + + /// Calculate the borsh-serialized size of a state with `beam_count` number of beams. + pub fn size(beam_count: usize) -> usize { + Self::SIZE_WITH_ZERO_BEAMS + (BeamEpochDetails::SIZE * beam_count) // allocations vec + } + + /// Calculate the size of a state account. + pub fn size_inner(&self) -> usize { + EpochReport::size(self.beam_epoch_details.len()) + } + + pub fn extractable_yield(&self) -> u64 { + self.beam_epoch_details + .iter() + .map(|x| x.extractable_yield) + .sum() + } + + pub fn extracted_yield(&self) -> u64 { + self.beam_epoch_details + .iter() + .map(|x| x.extracted_yield) + .sum() + } + + pub fn is_epoch_reported(&self, epoch: u64) -> bool { + self.beam_epoch_details.iter().all(|x| x.epoch == epoch) + } + + pub fn is_epoch_reported_for_beam_idx(&self, epoch: u64, beam_idx: usize) -> bool { + self.beam_epoch_details[beam_idx].epoch == epoch + } + + pub fn extract_yield_for_beam( + &mut self, + beam_idx: usize, + yield_amount: u64, + epoch: u64, + ) -> Result<()> { + let beam_details = &mut self.beam_epoch_details[beam_idx]; + + // The epoch report must be already updated for this epoch and beam + require!( + beam_details.epoch == epoch, + BeamError::EpochReportNotUpToDate + ); + + beam_details.extracted_yield = beam_details + .extracted_yield + .checked_add(yield_amount) + .ok_or(BeamError::Overflow) + .unwrap(); + + Ok(()) + } + + pub fn update_extractable_yield_and_epoch_for_beam( + &mut self, + beam_idx: usize, + epoch: u64, + extractable_yield: u64, + ) { + self.beam_epoch_details[beam_idx].epoch = epoch; + self.beam_epoch_details[beam_idx].extractable_yield = extractable_yield; + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct BeamEpochDetails { + /// The most recent epoch that this beam has reported its extractable yield for pub epoch: u64, pub extractable_yield: u64, pub extracted_yield: u64, - pub current_gsol_supply: u64, - pub bump: u8, } -impl EpochReport { - pub const SIZE: usize = 8 + // discriminator - 8 + // epoch - 8 + // extractable_yield - 8 + // extracted_yield - 8 + // current_gsol_supply - 1; // bump +impl BeamEpochDetails { + pub const SIZE: usize = 8 + // epoch + 8 + // extractable_yield + 8; // extracted_yield + + pub fn new(epoch: u64) -> BeamEpochDetails { + BeamEpochDetails { + epoch, + extractable_yield: 0, + extracted_yield: 0, + } + } } #[cfg(test)] @@ -259,7 +353,7 @@ mod internal_tests { let mut input = RegisterStateInput::default(); input.initial_capacity = 10; - state.register(input, 0, &Pubkey::default(), 1000, 0); + state.register(input, 0, &Pubkey::default(), 1000); assert_eq!(state.allocations, vec![BeamDetails::default(); 10]); } #[test] @@ -298,7 +392,7 @@ mod internal_tests { let mut input = RegisterStateInput::default(); input.initial_capacity = 2; - state.register(input, 0, &Pubkey::new_unique(), 1000, 0); + state.register(input, 0, &Pubkey::new_unique(), 1000); let beam_key = Pubkey::new_unique(); let new_beam = BeamDetails::new(beam_key, 10); @@ -314,13 +408,13 @@ mod internal_tests { ) ); - assert!(state.contains_beam(&beam_key) == true); - assert!(state.beam_count() == 1); + assert_eq!(state.contains_beam(&beam_key), true); + assert_eq!(state.beam_count(), 1); assert!(state .add_beam(BeamDetails::new(Pubkey::new_unique(), 20)) .is_ok()); - assert!(state.beam_count() == 2); + assert_eq!(state.beam_count(), 2); // Should fail because no space let expect_to_fail2 = state.add_beam(BeamDetails::new(Pubkey::new_unique(), 20)); @@ -388,15 +482,15 @@ mod internal_tests { )); if let Some(res) = state.get_beam_details(&key) { - assert!(res.key == key); - assert!(res.allocation == 90); + assert_eq!(res.key, key); + assert_eq!(res.allocation, 90); } else { panic!("") } if let Some(res) = state.get_mut_beam_details(&key) { - assert!(res.key == key); - assert!(res.allocation == 90); + assert_eq!(res.key, key); + assert_eq!(res.allocation, 90); } else { panic!("") } @@ -406,13 +500,13 @@ mod internal_tests { let mut state = State::default(); let initial_size = state.size_inner(); - assert!(state.allocations.len() == 0); - assert!(initial_size == State::SIZE_WITH_ZERO_BEAMS); - assert!(initial_size == State::size(0)); + assert_eq!(state.allocations.len(), 0); + assert_eq!(initial_size, State::SIZE_WITH_ZERO_BEAMS); + assert_eq!(initial_size, State::size(0)); state.allocations = vec![BeamDetails::default(); 10]; let size = state.size_inner(); - assert!(size == State::size(10)); - assert!(size == initial_size + (10 * BeamDetails::SIZE)) + assert_eq!(size, State::size(10)); + assert_eq!(size, initial_size + (10 * BeamDetails::SIZE)) } } diff --git a/programs/sunrise-core/src/system.rs b/programs/sunrise-core/src/system.rs index 2437ca2..e656701 100644 --- a/programs/sunrise-core/src/system.rs +++ b/programs/sunrise-core/src/system.rs @@ -1,20 +1,18 @@ use crate::{state::State, BeamError}; -use anchor_lang::prelude::*; +use anchor_lang::prelude::{Account, AccountInfo, Pubkey}; /// Verifies that a mint request is valid by: /// - Checking that the beam is present in the state. /// - Checking that the executing program owns the beam. -pub fn check_beam_validity( +pub fn checked_find_beam_idx( state: &Account<'_, State>, beam: &AccountInfo, cpi_program_id: &Pubkey, -) -> Result<()> { - if !state.contains_beam(beam.key) { - return Err(BeamError::UnidentifiedBeam.into()); - } +) -> Result { if beam.owner != cpi_program_id { - return Err(BeamError::UnidentifiedCallingProgram.into()); + return Err(BeamError::UnidentifiedCallingProgram); } - Ok(()) + let index = state.find_beam_index(beam.key); + index.ok_or(BeamError::UnidentifiedBeam) } diff --git a/programs/sunrise-core/tests/helpers/context.rs b/programs/sunrise-core/tests/helpers/context.rs index 7f049a1..4244e58 100644 --- a/programs/sunrise-core/tests/helpers/context.rs +++ b/programs/sunrise-core/tests/helpers/context.rs @@ -13,7 +13,6 @@ pub struct SunriseContext { pub update_authority: Keypair, pub state: Pubkey, pub gsol_mint_authority: Option<(Pubkey, u8)>, - pub epoch_report: Option<(Pubkey, u8)>, } impl SunriseContext { @@ -26,12 +25,10 @@ impl SunriseContext { initial_capacity: u8, ) -> Result { let gsol_mint_authority = Self::find_gsol_mint_authority_pda(&state.pubkey()); - let epoch_report = Self::find_epoch_report_pda(&state.pubkey()); let (_, instruction) = register_state( &ctx.payer.pubkey(), &gsol_mint.pubkey(), - &epoch_report.0, update_authority, &state.pubkey(), yield_account, @@ -43,7 +40,6 @@ impl SunriseContext { ctx: RefCell::new(ctx), state: state.pubkey(), gsol_mint_authority: Some(gsol_mint_authority), - epoch_report: Some(epoch_report), update_authority: Keypair::new(), }; @@ -180,16 +176,4 @@ impl SunriseContext { let seeds = &[state.as_ref(), sunrise_core::seeds::GSOL_AUTHORITY]; Pubkey::find_program_address(seeds, &sunrise_core::id()) } - - #[allow(dead_code)] - fn epoch_report(&self) -> Pubkey { - self.epoch_report - .map(|a| a.0) - .unwrap_or(Self::find_epoch_report_pda(&self.state).0) - } - - pub fn find_epoch_report_pda(state: &Pubkey) -> (Pubkey, u8) { - let seeds = &[state.as_ref(), sunrise_core::seeds::EPOCH_REPORT]; - Pubkey::find_program_address(seeds, &sunrise_core::id()) - } } diff --git a/programs/sunrise-core/tests/helpers/instructions.rs b/programs/sunrise-core/tests/helpers/instructions.rs index e5b158a..6165cdd 100644 --- a/programs/sunrise-core/tests/helpers/instructions.rs +++ b/programs/sunrise-core/tests/helpers/instructions.rs @@ -5,7 +5,6 @@ use sunrise_core::{accounts as sunrise_accounts, instruction as sunrise_instruct pub fn register_state( payer: &Pubkey, gsol_mint: &Pubkey, - epoch_report: &Pubkey, update_authority: &Pubkey, state: &Pubkey, yield_account: &Pubkey, @@ -15,7 +14,6 @@ pub fn register_state( let accounts = sunrise_accounts::RegisterState { payer: *payer, state: *state, - epoch_report: *epoch_report, gsol_mint: *gsol_mint, gsol_mint_authority: *gsol_mint_auth_pda, system_program: system_program::id(), From 4dcdb89cff096d67cb90fdeae7d6f620d24abca1 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Mon, 12 Feb 2024 14:13:02 +0100 Subject: [PATCH 4/8] Marinade LP: implement extract yield and update epoch record --- Cargo.lock | 10 + lib/marinade-common/Cargo.toml | 10 + lib/marinade-common/src/lib.rs | 73 ++++ .../src/vault_authority_seed.rs | 33 ++ .../sdks/common/src/types/marinade_lp_beam.ts | 360 +++++++++++++++++- .../sdks/common/src/types/sunrise_core.ts | 10 - programs/marinade-beam/Cargo.toml | 1 + .../src/cpi_interface/marinade.rs | 2 +- .../marinade-beam/src/cpi_interface/mod.rs | 1 - .../src/cpi_interface/sunrise.rs | 1 - .../src/cpi_interface/vault_authority_seed.rs | 27 -- programs/marinade-beam/src/lib.rs | 14 +- programs/marinade-beam/src/state.rs | 7 + programs/marinade-beam/src/system/utils.rs | 67 +--- programs/marinade-lp-beam/Cargo.toml | 3 +- .../src/cpi_interface/marinade_lp.rs | 44 ++- .../marinade-lp-beam/src/cpi_interface/mod.rs | 1 + .../src/cpi_interface/program.rs | 11 + .../src/cpi_interface/sunrise.rs | 60 ++- programs/marinade-lp-beam/src/lib.rs | 228 ++++++++++- programs/marinade-lp-beam/src/state.rs | 7 + .../marinade-lp-beam/src/system/balance.rs | 21 +- programs/marinade-lp-beam/src/system/utils.rs | 49 +-- .../spl-beam/src/cpi_interface/sunrise.rs | 1 - .../src/instructions/extract_yield.rs | 2 +- programs/sunrise-core/src/lib.rs | 2 - 26 files changed, 846 insertions(+), 199 deletions(-) create mode 100644 lib/marinade-common/Cargo.toml create mode 100644 lib/marinade-common/src/lib.rs create mode 100644 lib/marinade-common/src/vault_authority_seed.rs delete mode 100644 programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs create mode 100644 programs/marinade-lp-beam/src/cpi_interface/program.rs diff --git a/Cargo.lock b/Cargo.lock index c395242..821c5bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2272,10 +2272,19 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "marinade-common", "marinade-cpi", "sunrise-core", ] +[[package]] +name = "marinade-common" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "marinade-cpi", +] + [[package]] name = "marinade-cpi" version = "0.4.0" @@ -2291,6 +2300,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "marinade-common", "marinade-cpi", "sunrise-core", ] diff --git a/lib/marinade-common/Cargo.toml b/lib/marinade-common/Cargo.toml new file mode 100644 index 0000000..3f0265d --- /dev/null +++ b/lib/marinade-common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "marinade-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anchor-lang = '0.29.0' +marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } \ No newline at end of file diff --git a/lib/marinade-common/src/lib.rs b/lib/marinade-common/src/lib.rs new file mode 100644 index 0000000..5966ef0 --- /dev/null +++ b/lib/marinade-common/src/lib.rs @@ -0,0 +1,73 @@ +pub mod vault_authority_seed; + +use anchor_lang::prelude::*; +use marinade_cpi::state::State as MarinadeState; +use std::num::TryFromIntError; + +/// calculate amount*numerator/denominator +/// as value = shares * share_price where share_price=total_value/total_shares +/// or shares = amount_value / share_price where share_price=total_value/total_shares +/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value +pub fn proportional( + amount: u64, + numerator: u64, + denominator: u64, +) -> std::result::Result { + if denominator == 0 { + return Ok(amount); + } + u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) +} + +// All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 +pub fn calc_msol_from_lamports( + marinade_state: &MarinadeState, + stake_lamports: u64, +) -> std::result::Result { + msg!("calc_msol_from_lamports"); + msg!("stake_lamports: {}", stake_lamports); + msg!("marinade_state.msol_supply: {}", marinade_state.msol_supply); + msg!( + "total_virtual_staked_lamports: {}", + total_virtual_staked_lamports(marinade_state) + ); + proportional( + stake_lamports, + marinade_state.msol_supply, + total_virtual_staked_lamports(marinade_state), + ) +} +pub fn calc_lamports_from_msol_amount( + marinade_state: &MarinadeState, + msol_amount: u64, +) -> std::result::Result { + proportional( + msol_amount, + total_virtual_staked_lamports(marinade_state), + marinade_state.msol_supply, + ) +} + +fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { + marinade_state + .stake_system + .delayed_unstake_cooling_down + .checked_add(marinade_state.emergency_cooling_down) + .expect("Total cooling down overflow") +} + +fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { + marinade_state + .validator_system + .total_active_balance + .checked_add(total_cooling_down(marinade_state)) + .expect("Stake balance overflow") + .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc + .expect("Total SOLs under control overflow") +} + +fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { + // if we get slashed it may be negative but we must use 0 instead + total_lamports_under_control(marinade_state) + .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet +} diff --git a/lib/marinade-common/src/vault_authority_seed.rs b/lib/marinade-common/src/vault_authority_seed.rs new file mode 100644 index 0000000..b8b0aa6 --- /dev/null +++ b/lib/marinade-common/src/vault_authority_seed.rs @@ -0,0 +1,33 @@ +use anchor_lang::prelude::{Account, Key}; +use anchor_lang::{AccountDeserialize, AccountSerialize}; + +// NOTE: this must match the constant used by the programs themselves +const VAULT_AUTHORITY: &[u8] = b"vault-authority"; + +pub struct VaultAuthoritySeed<'a> { + state_address: Vec, + vault_authority: &'a [u8], + bump: Vec, +} + +pub trait HasVaultAuthority: AccountSerialize + AccountDeserialize + Clone { + fn vault_authority_bump(&self) -> u8; +} + +impl<'a> VaultAuthoritySeed<'a> { + pub fn new<'info>(state: &'a Account<'info, impl HasVaultAuthority>) -> Self { + let state_address = state.key().to_bytes().to_vec(); + let vault_authority = VAULT_AUTHORITY; + let bump = vec![state.vault_authority_bump()]; + + VaultAuthoritySeed { + state_address, + vault_authority, + bump, + } + } + + pub fn as_slices(&self) -> [&[u8]; 3] { + [&self.state_address, self.vault_authority, &self.bump] + } +} diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index accc1e3..cf063aa 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -86,7 +86,7 @@ export type MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -195,7 +195,7 @@ export type MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -372,6 +372,182 @@ export type MarinadeLpBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "transferMsolTo", + "isMut": true, + "isSigner": false, + "docs": [ + "When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL.", + "The SOL portion is transferred to the user (withdrawer) and the mSOL portion", + "is transferred to the msol_token_account owned by the marinade stake pool." + ] + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -563,7 +739,7 @@ export const IDL: MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -672,7 +848,7 @@ export const IDL: MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -849,6 +1025,182 @@ export const IDL: MarinadeLpBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "transferMsolTo", + "isMut": true, + "isSigner": false, + "docs": [ + "When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL.", + "The SOL portion is transferred to the user (withdrawer) and the mSOL portion", + "is transferred to the msol_token_account owned by the marinade stake pool." + ] + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index cf657a7..62dbb1d 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -397,11 +397,6 @@ export type SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -1138,11 +1133,6 @@ export const IDL: SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, diff --git a/programs/marinade-beam/Cargo.toml b/programs/marinade-beam/Cargo.toml index 707c424..17a5051 100644 --- a/programs/marinade-beam/Cargo.toml +++ b/programs/marinade-beam/Cargo.toml @@ -20,3 +20,4 @@ anchor-lang = '0.29.0' anchor-spl = '0.29.0' marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } sunrise-core = { path = "../sunrise-core", features = ["cpi"] } +marinade-common = { path = "../../lib/marinade-common" } diff --git a/programs/marinade-beam/src/cpi_interface/marinade.rs b/programs/marinade-beam/src/cpi_interface/marinade.rs index e2e15e9..a29eed5 100644 --- a/programs/marinade-beam/src/cpi_interface/marinade.rs +++ b/programs/marinade-beam/src/cpi_interface/marinade.rs @@ -1,8 +1,8 @@ use crate::cpi_interface::program::Marinade; -use crate::cpi_interface::vault_authority_seed::VaultAuthoritySeed; use crate::state::State; use crate::{ExtractYield, OrderWithdrawal, Withdraw}; use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::VaultAuthoritySeed; use marinade_cpi::cpi::{ accounts::{ Claim as MarinadeClaim, Deposit as MarinadeDeposit, diff --git a/programs/marinade-beam/src/cpi_interface/mod.rs b/programs/marinade-beam/src/cpi_interface/mod.rs index 3b0ecd6..346ead8 100644 --- a/programs/marinade-beam/src/cpi_interface/mod.rs +++ b/programs/marinade-beam/src/cpi_interface/mod.rs @@ -1,4 +1,3 @@ pub mod marinade; pub mod program; pub mod sunrise; -mod vault_authority_seed; diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index 27c1b91..0b6455c 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -133,7 +133,6 @@ impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), - sysvar_clock: accounts.sysvar_clock.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } diff --git a/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs b/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs deleted file mode 100644 index c90e1cf..0000000 --- a/programs/marinade-beam/src/cpi_interface/vault_authority_seed.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state::State; -use anchor_lang::prelude::Account; -use anchor_lang::Key; - -pub struct VaultAuthoritySeed<'a> { - state_address: Vec, - vault_authority: &'a [u8], - bump: Vec, -} - -impl<'a> VaultAuthoritySeed<'a> { - pub fn new<'info>(state: &'a Account<'info, State>) -> Self { - let state_address = state.key().to_bytes().to_vec(); - let vault_authority = crate::constants::VAULT_AUTHORITY; - let bump = vec![state.vault_authority_bump]; - - VaultAuthoritySeed { - state_address, - vault_authority, - bump, - } - } - - pub fn as_slices(&self) -> [&[u8]; 3] { - [&self.state_address, self.vault_authority, &self.bump] - } -} diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index dda4bdb..989035e 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -39,6 +39,7 @@ mod constants { pub mod marinade_beam { use super::*; use crate::cpi_interface::marinade; + use marinade_common::calc_msol_from_lamports; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -100,8 +101,8 @@ pub mod marinade_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = - utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports) + .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; msg!("Liquid Unstake {} msol", msol_lamports); // CPI: Liquid unstake. @@ -129,8 +130,9 @@ pub mod marinade_beam { pub fn order_withdrawal(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = - utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports) + .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; + // CPI: Order unstake and receive a Marinade unstake ticket. let accounts = ctx.accounts.deref().into(); marinade_interface::order_unstake( @@ -209,8 +211,8 @@ pub mod marinade_beam { &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, )?; - let yield_msol = - utils::calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports)?; + let yield_msol = calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports) + .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; let yield_account_balance_before = ctx.accounts.yield_account.lamports(); diff --git a/programs/marinade-beam/src/state.rs b/programs/marinade-beam/src/state.rs index 91cb12f..03b71a4 100644 --- a/programs/marinade-beam/src/state.rs +++ b/programs/marinade-beam/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::HasVaultAuthority; #[account] pub struct State { @@ -16,6 +17,12 @@ pub struct State { pub vault_authority_bump: u8, } +impl HasVaultAuthority for State { + fn vault_authority_bump(&self) -> u8 { + self.vault_authority_bump + } +} + // Anchor-ts only supports deserialization(in instruction arguments) for types // that explicitly derive AnchorSerialize & AnchorDeserialize. // https://github.com/coral-xyz/anchor/issues/2545 diff --git a/programs/marinade-beam/src/system/utils.rs b/programs/marinade-beam/src/system/utils.rs index aa3a039..e7e4153 100644 --- a/programs/marinade-beam/src/system/utils.rs +++ b/programs/marinade-beam/src/system/utils.rs @@ -4,6 +4,7 @@ use anchor_lang::solana_program::{ borsh0_10::try_from_slice_unchecked, stake::state::StakeStateV2, }; use anchor_spl::token::TokenAccount; +use marinade_common::calc_lamports_from_msol_amount; use marinade_cpi::state::State as MarinadeState; use sunrise_core::BeamError; @@ -14,7 +15,8 @@ pub fn calculate_extractable_yield( marinade_state: &MarinadeState, msol_vault: &TokenAccount, ) -> Result { - let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount)?; + let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount) + .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; @@ -22,45 +24,6 @@ pub fn calculate_extractable_yield( Ok(staked_value.saturating_sub(staked_sol)) } -/// calculate amount*numerator/denominator -/// as value = shares * share_price where share_price=total_value/total_shares -/// or shares = amount_value / share_price where share_price=total_value/total_shares -/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value -pub fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result { - if denominator == 0 { - return Ok(amount); - } - u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure)) -} - -// All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 -pub fn calc_msol_from_lamports(marinade_state: &MarinadeState, stake_lamports: u64) -> Result { - msg!("calc_msol_from_lamports"); - msg!("stake_lamports: {}", stake_lamports); - msg!("marinade_state.msol_supply: {}", marinade_state.msol_supply); - msg!( - "total_virtual_staked_lamports: {}", - total_virtual_staked_lamports(marinade_state) - ); - proportional( - stake_lamports, - marinade_state.msol_supply, - total_virtual_staked_lamports(marinade_state), - ) -} - -pub fn calc_lamports_from_msol_amount( - marinade_state: &MarinadeState, - msol_amount: u64, -) -> Result { - proportional( - msol_amount, - total_virtual_staked_lamports(marinade_state), - marinade_state.msol_supply, - ) -} - pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { // Gets the active stake amount of the stake account. We need this to determine how much gSol to mint. let stake_state = try_from_slice_unchecked::(&stake_account.data.borrow())?; @@ -70,27 +33,3 @@ pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { None => Err(crate::MarinadeBeamError::NotDelegated.into()), } } - -fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { - marinade_state - .stake_system - .delayed_unstake_cooling_down - .checked_add(marinade_state.emergency_cooling_down) - .expect("Total cooling down overflow") -} - -fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { - marinade_state - .validator_system - .total_active_balance - .checked_add(total_cooling_down(marinade_state)) - .expect("Stake balance overflow") - .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc - .expect("Total SOLs under control overflow") -} - -fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { - // if we get slashed it may be negative but we must use 0 instead - total_lamports_under_control(marinade_state) - .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet -} diff --git a/programs/marinade-lp-beam/Cargo.toml b/programs/marinade-lp-beam/Cargo.toml index dab7720..14a3e05 100644 --- a/programs/marinade-lp-beam/Cargo.toml +++ b/programs/marinade-lp-beam/Cargo.toml @@ -19,4 +19,5 @@ default = [] anchor-lang = '0.29.0' anchor-spl = '0.29.0' marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } -sunrise-core = { path = "../sunrise-core", features = ["cpi"] } \ No newline at end of file +sunrise-core = { path = "../sunrise-core", features = ["cpi"] } +marinade-common = { path = "../../lib/marinade-common" } \ No newline at end of file diff --git a/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs b/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs index b662635..a8267bb 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs @@ -1,4 +1,7 @@ +use crate::cpi_interface::program::Marinade; +use crate::state::State; use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::VaultAuthoritySeed; use marinade_cpi::cpi::{ accounts::{AddLiquidity as MarinadeAddLiquidity, RemoveLiquidity as MarinadeRemoveLiquidity}, add_liquidity as marinade_add_liquidity, remove_liquidity as marinade_remove_liquidity, @@ -19,18 +22,19 @@ pub fn add_liquidity( Ok(()) } -pub fn remove_liquidity(accounts: &crate::Withdraw, liq_pool_tokens_amount: u64) -> Result<()> { - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; +pub fn remove_liquidity<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeRemoveLiquidity<'info>, + liq_pool_tokens_amount: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_ctx = CpiContext::new(cpi_program, accounts.into()); - marinade_remove_liquidity(cpi_ctx.with_signer(&[seeds]), liq_pool_tokens_amount)?; + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + marinade_remove_liquidity(cpi_ctx.with_signer(&[&seeds[..]]), liq_pool_tokens_amount)?; Ok(()) } @@ -68,3 +72,21 @@ impl<'a> From<&crate::Withdraw<'a>> for MarinadeRemoveLiquidity<'a> { } } } + +impl<'a> From<&crate::ExtractYield<'a>> for MarinadeRemoveLiquidity<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> MarinadeRemoveLiquidity<'a> { + Self { + state: accounts.marinade_state.to_account_info(), + lp_mint: accounts.liq_pool_mint.to_account_info(), + burn_from: accounts.liq_pool_token_vault.to_account_info(), + burn_from_authority: accounts.vault_authority.to_account_info(), + transfer_sol_to: accounts.yield_account.to_account_info(), + transfer_msol_to: accounts.transfer_msol_to.to_account_info(), + liq_pool_sol_leg_pda: accounts.liq_pool_sol_leg_pda.to_account_info(), + liq_pool_msol_leg: accounts.liq_pool_msol_leg.to_account_info(), + liq_pool_msol_leg_authority: accounts.liq_pool_msol_leg_authority.to_account_info(), + system_program: accounts.system_program.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} diff --git a/programs/marinade-lp-beam/src/cpi_interface/mod.rs b/programs/marinade-lp-beam/src/cpi_interface/mod.rs index 8204ba7..0e040a1 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/mod.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/mod.rs @@ -1,2 +1,3 @@ pub mod marinade_lp; +pub mod program; pub mod sunrise; diff --git a/programs/marinade-lp-beam/src/cpi_interface/program.rs b/programs/marinade-lp-beam/src/cpi_interface/program.rs new file mode 100644 index 0000000..ec627d3 --- /dev/null +++ b/programs/marinade-lp-beam/src/cpi_interface/program.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::Id; + +#[derive(Clone)] +pub struct Marinade; + +impl Id for Marinade { + fn id() -> Pubkey { + marinade_cpi::ID + } +} diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index a234ed8..c657a10 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -1,9 +1,12 @@ use anchor_lang::prelude::*; // TODO: Use actual CPI crate. +use crate::constants::STATE; use sunrise_core as sunrise_core_cpi; +use sunrise_core::cpi::accounts::{ExtractYield, UpdateEpochReport}; use sunrise_core_cpi::cpi::{ accounts::{BurnGsol, MintGsol}, - burn_gsol as cpi_burn_gsol, mint_gsol as cpi_mint_gsol, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -81,3 +84,58 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { } } } + +pub fn extract_yield<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: ExtractYield<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_extract_yield( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index 62a5318..8c8a909 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -18,6 +18,7 @@ use state::{State, StateEntry}; use system::utils; // TODO: Use actual CPI crate. +use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; declare_id!("9Xek4q2hsdPm4yaRt4giQnVTTgRGwGhXQ1HBXbinuPTP"); @@ -32,6 +33,8 @@ mod constants { #[program] pub mod marinade_lp_beam { use super::*; + use crate::cpi_interface::marinade_lp; + use crate::system::utils::calc_liq_pool_tokens_from_lamports; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -73,7 +76,7 @@ pub mod marinade_lp_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate the number of liq_pool tokens `lamports` is worth. - let liq_pool_tokens = utils::liq_pool_tokens_from_lamports( + let liq_pool_tokens = utils::calc_liq_pool_tokens_from_lamports( &ctx.accounts.marinade_state, &ctx.accounts.liq_pool_mint, &ctx.accounts.liq_pool_sol_leg_pda, @@ -81,7 +84,13 @@ pub mod marinade_lp_beam { )?; // CPI: Remove liquidity from Marinade liq_pool. The liq_pool tokens vault owned by // this vault is the source burning lp tokens. - marinade_lp_interface::remove_liquidity(ctx.accounts, liq_pool_tokens)?; + let accounts = ctx.accounts.deref().into(); + marinade_lp::remove_liquidity( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + liq_pool_tokens, + )?; let state_bump = ctx.bumps.state; // CPI: Burn GSOL of the same proportion as the lamports withdrawn from the depositor. @@ -121,6 +130,81 @@ pub mod marinade_lp_beam { // Marinade liq_pool only supports immediate withdrawals. Err(MarinadeLpBeamError::Unimplemented.into()) } + + pub fn extract_yield(ctx: Context) -> Result<()> { + let yield_lamports = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_token_vault, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + )?; + + let yield_liq_pool_tokens = calc_liq_pool_tokens_from_lamports( + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_sol_leg_pda, + yield_lamports, + )?; + + let yield_account_balance_before = ctx.accounts.yield_account.lamports(); + + let accounts = ctx.accounts.deref().into(); + marinade_lp::remove_liquidity( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + yield_liq_pool_tokens, + )?; + + let yield_account_balance_after = ctx.accounts.yield_account.lamports(); + let withdrawn_lamports = + yield_account_balance_after.saturating_sub(yield_account_balance_before); + + msg!("Withdrawn {} lamports to yield account", withdrawn_lamports); + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::extract_yield( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + withdrawn_lamports, + )?; + + Ok(()) + } + + pub fn update_epoch_report(ctx: Context) -> Result<()> { + let yield_lamports = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_token_vault, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + )?; + + // Reduce by fee + // TODO can we do better than an estimate? + let extractable_lamports = (yield_lamports as f64) * 0.997; // estimated 0.3% unstake fee + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + extractable_lamports as u64, + )?; + + Ok(()) + } } #[derive(Accounts)] @@ -171,7 +255,6 @@ pub struct Update<'info> { #[derive(Accounts)] pub struct Deposit<'info> { #[account( - mut, has_one = sunrise_state, has_one = marinade_state, seeds = [constants::STATE, sunrise_state.key().as_ref()], @@ -182,8 +265,7 @@ pub struct Deposit<'info> { /// CHECK: The registered Marinade state. pub marinade_state: UncheckedAccount<'info>, #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub depositor: Signer<'info>, @@ -231,15 +313,12 @@ pub struct Deposit<'info> { pub token_program: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade ProgramID. - pub marinade_program: UncheckedAccount<'info>, + pub marinade_program: Program<'info, Marinade>, } #[derive(Accounts)] pub struct Withdraw<'info> { #[account( - mut, has_one = sunrise_state, has_one = marinade_state, seeds = [constants::STATE, sunrise_state.key().as_ref()], @@ -301,9 +380,7 @@ pub struct Withdraw<'info> { pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, + pub marinade_program: Program<'info, Marinade>, } #[derive(Accounts)] @@ -316,8 +393,7 @@ pub struct Burn<'info> { )] pub state: Box>, #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub burner: Signer<'info>, @@ -338,6 +414,130 @@ pub struct Burn<'info> { pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } +#[derive(Accounts)] +pub struct ExtractYield<'info> { + #[account( + has_one = sunrise_state, + has_one = marinade_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + #[account(mut)] + pub marinade_state: Box>, + #[account( + mut, // Update the extracted yield on the state's epoch report. + has_one = yield_account + )] + pub sunrise_state: Box>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(mut)] + /// CHECK: Matches the yield account key stored in the state. + pub yield_account: UncheckedAccount<'info>, + + #[account(mut)] + pub liq_pool_mint: Box>, + #[account( + mut, + token::mint = liq_pool_mint, + token::authority = vault_authority, + )] + pub liq_pool_token_vault: Box>, + #[account( + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + /// When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL. + /// The SOL portion is transferred to the user (withdrawer) and the mSOL portion + /// is transferred to the msol_token_account owned by the marinade stake pool. + #[account(mut, address = state.msol_token_account)] + pub transfer_msol_to: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub system_program: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub token_program: UncheckedAccount<'info>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub marinade_program: Program<'info, Marinade>, +} + +#[derive(Accounts)] +pub struct UpdateEpochReport<'info> { + #[account( + has_one = sunrise_state, + has_one = marinade_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + pub marinade_state: Box>, + #[account( + mut, // Update the extracted yield on the state's epoch report. + )] + pub sunrise_state: Box>, + + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + #[account(mut)] + pub liq_pool_mint: Box>, + #[account( + mut, + token::mint = liq_pool_mint, + token::authority = vault_authority, + )] + pub liq_pool_token_vault: Box>, + #[account( + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub system_program: UncheckedAccount<'info>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub marinade_program: Program<'info, Marinade>, +} + #[derive(Accounts)] pub struct Noop {} diff --git a/programs/marinade-lp-beam/src/state.rs b/programs/marinade-lp-beam/src/state.rs index cbb4507..3b9f487 100644 --- a/programs/marinade-lp-beam/src/state.rs +++ b/programs/marinade-lp-beam/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::HasVaultAuthority; #[account] pub struct State { @@ -22,6 +23,12 @@ pub struct State { pub msol_token_account: Pubkey, } +impl HasVaultAuthority for State { + fn vault_authority_bump(&self) -> u8 { + self.vault_authority_bump + } +} + impl State { pub const SPACE: usize = 8 + /*discriminator*/ 32 + /*update_authority*/ diff --git a/programs/marinade-lp-beam/src/system/balance.rs b/programs/marinade-lp-beam/src/system/balance.rs index 9defc6b..07d6dbc 100644 --- a/programs/marinade-lp-beam/src/system/balance.rs +++ b/programs/marinade-lp-beam/src/system/balance.rs @@ -1,5 +1,5 @@ -use super::utils::{calc_lamports_from_msol_amount, proportional}; use anchor_lang::prelude::*; +use marinade_common::{calc_lamports_from_msol_amount, proportional}; use marinade_cpi::State as MarinadeState; #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -18,8 +18,10 @@ impl LiquidityPoolBalance { } pub fn value_of(&self, liq_pool_token: u64) -> Result { - let lamports = proportional(self.lamports, liq_pool_token, self.liq_pool_token)?; - let msol = proportional(self.msol, liq_pool_token, self.liq_pool_token)?; + let lamports = proportional(self.lamports, liq_pool_token, self.liq_pool_token) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let msol = proportional(self.msol, liq_pool_token, self.liq_pool_token) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; Ok(LiquidityPoolBalance { lamports, msol, @@ -42,9 +44,10 @@ impl LiquidityPoolBalance { if self.lamports < other_lamports { return Ok(*self); } - let other_liq_pool_token = - proportional(self.liq_pool_token, other_lamports, self.lamports)?; - let other_msol = proportional(self.msol, other_lamports, self.lamports)?; + let other_liq_pool_token = proportional(self.liq_pool_token, other_lamports, self.lamports) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let other_msol = proportional(self.msol, other_lamports, self.lamports) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; Ok(Self { lamports: other_lamports, msol: other_msol, @@ -58,9 +61,11 @@ impl LiquidityPoolBalance { .lamports .checked_sub(other_lamports) .expect("checked_sub_lamports"); - let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports)?; + let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - let new_msol = proportional(self.msol, new_lamports, self.lamports)?; + let new_msol = proportional(self.msol, new_lamports, self.lamports) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; Ok(Self { lamports: new_lamports, msol: new_msol, diff --git a/programs/marinade-lp-beam/src/system/utils.rs b/programs/marinade-lp-beam/src/system/utils.rs index cd20cd9..9cc2a78 100644 --- a/programs/marinade-lp-beam/src/system/utils.rs +++ b/programs/marinade-lp-beam/src/system/utils.rs @@ -2,21 +2,10 @@ use super::balance::LiquidityPoolBalance; use crate::state::State; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; +use marinade_common::proportional; use marinade_cpi::State as MarinadeState; use sunrise_core::BeamError; -/// calculate amount*numerator/denominator -/// as value = shares * share_price where share_price=total_value/total_shares -/// or shares = amount_value / share_price where share_price=total_value/total_shares -/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value -pub(super) fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result { - if denominator == 0 { - return Ok(amount); - } - u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) -} - /// Calculates the amount that can be extracted as yield, in lamports. pub fn calculate_extractable_yield( sunrise_state: &sunrise_core::State, @@ -95,7 +84,7 @@ fn total_liq_pool( ) } -pub fn liq_pool_tokens_from_lamports( +pub fn calc_liq_pool_tokens_from_lamports( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_sol_leg_pda: &AccountInfo, @@ -108,37 +97,5 @@ pub fn liq_pool_tokens_from_lamports( let liq_pool_mint_supply = liq_pool_mint.supply; proportional(liq_pool_mint_supply, lamports, liq_pool_lamports) -} - -// The following are lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 -pub fn calc_lamports_from_msol_amount( - marinade_state: &MarinadeState, - msol_amount: u64, -) -> Result { - proportional( - msol_amount, - total_virtual_staked_lamports(marinade_state), - marinade_state.msol_supply, - ) -} -fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { - marinade_state - .validator_system - .total_active_balance - .checked_add(total_cooling_down(marinade_state)) - .expect("Stake balance overflow") - .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc - .expect("Total SOLs under control overflow") -} -fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { - // if we get slashed it may be negative but we must use 0 instead - total_lamports_under_control(marinade_state) - .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet -} -fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { - marinade_state - .stake_system - .delayed_unstake_cooling_down - .checked_add(marinade_state.emergency_cooling_down) - .expect("Total cooling down overflow") + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) } diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index fbba535..e16c7c8 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -150,7 +150,6 @@ impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), - sysvar_clock: accounts.sysvar_clock.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } diff --git a/programs/sunrise-core/src/instructions/extract_yield.rs b/programs/sunrise-core/src/instructions/extract_yield.rs index e802a53..d1eacec 100644 --- a/programs/sunrise-core/src/instructions/extract_yield.rs +++ b/programs/sunrise-core/src/instructions/extract_yield.rs @@ -8,7 +8,7 @@ use crate::{system, utils, BeamError, ExtractYield}; /// It only updates the extracted yield on the epoch report. pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { let state = &mut ctx.accounts.state; - let current_epoch = ctx.accounts.sysvar_clock.epoch; + let current_epoch = Clock::get()?.epoch; // Check that the executing program is valid. let cpi_program = diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index a724a92..8d85462 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -309,8 +309,6 @@ pub struct ExtractYield<'info> { /// This is verified in the handler to be a beam attached to this state. pub beam: Signer<'info>, - pub sysvar_clock: Sysvar<'info, Clock>, - /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] pub sysvar_instructions: UncheckedAccount<'info>, From 0e657b257f592178ed3596a01665571e4cbfbde8 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Fri, 23 Feb 2024 18:05:16 +0100 Subject: [PATCH 5/8] Marinade LP: WIP new instruction to transfer gsol from the LP to the SP on extract yield, so that the msol extracted and passed to the SP is represented in the SP's gsol balance --- package.json | 2 +- .../sdks/common/src/types/marinade_beam.ts | 10 -- .../sdks/common/src/types/marinade_lp_beam.ts | 34 +---- .../sdks/common/src/types/sunrise_core.ts | 82 +++++++++++ packages/sdks/marinade-lp/src/index.ts | 136 +++++++++++++---- packages/sdks/marinade-lp/src/state.ts | 6 +- packages/sdks/marinade-lp/src/utils.ts | 6 + packages/sdks/marinade-sp/src/index.ts | 1 - packages/sdks/spl/src/index.ts | 23 +++ .../src/functional/beams/marinade-lp.test.ts | 138 ++++++++++++++---- .../src/functional/beams/marinade-sp.test.ts | 5 +- .../functional/beams/spl-stake-pool.test.ts | 30 +++- programs/marinade-beam/src/lib.rs | 2 - .../src/cpi_interface/sunrise.rs | 61 ++++++-- programs/marinade-lp-beam/src/lib.rs | 70 +++++---- programs/marinade-lp-beam/src/state.rs | 11 +- .../marinade-lp-beam/src/system/balance.rs | 25 +++- programs/marinade-lp-beam/src/system/utils.rs | 106 ++++++++++++-- programs/spl-beam/src/lib.rs | 13 +- .../src/instructions/burn_gsol.rs | 9 +- programs/sunrise-core/src/instructions/mod.rs | 2 + .../src/instructions/transfer_gsol.rs | 34 +++++ programs/sunrise-core/src/lib.rs | 30 ++++ yarn.lock | 18 +-- 24 files changed, 669 insertions(+), 185 deletions(-) create mode 100644 programs/sunrise-core/src/instructions/transfer_gsol.rs diff --git a/package.json b/package.json index 85cd709..3c9da39 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prettier": "^3.0.3", "ts-node": "^10.9.1", "turbo": "^1.10.16", - "typedoc": "^0.25.1", + "typedoc": "^0.25.8", "typescript": "^5.3.2" } } diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 263278a..77fa7bf 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -704,11 +704,6 @@ export type MarinadeBeam = { "isMut": true, "isSigner": false }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -1621,11 +1616,6 @@ export const IDL: MarinadeBeam = { "isMut": true, "isSigner": false }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index cf063aa..173daa5 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -526,11 +526,6 @@ export type MarinadeLpBeam = { "isMut": true, "isSigner": false }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -540,11 +535,6 @@ export type MarinadeLpBeam = { "name": "sunriseProgram", "isMut": false, "isSigner": false - }, - { - "name": "marinadeProgram", - "isMut": false, - "isSigner": false } ], "args": [] @@ -586,9 +576,10 @@ export type MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "docs": [ - "This state's SOL vault." + "The beam address of the recipient of msol when withdrawing liquidity.", + "Typically the marinade-sp beam" ], "type": "publicKey" }, @@ -626,7 +617,7 @@ export type MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "type": "publicKey" }, { @@ -1179,11 +1170,6 @@ export const IDL: MarinadeLpBeam = { "isMut": true, "isSigner": false }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -1193,11 +1179,6 @@ export const IDL: MarinadeLpBeam = { "name": "sunriseProgram", "isMut": false, "isSigner": false - }, - { - "name": "marinadeProgram", - "isMut": false, - "isSigner": false } ], "args": [] @@ -1239,9 +1220,10 @@ export const IDL: MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "docs": [ - "This state's SOL vault." + "The beam address of the recipient of msol when withdrawing liquidity.", + "Typically the marinade-sp beam" ], "type": "publicKey" }, @@ -1279,7 +1261,7 @@ export const IDL: MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "type": "publicKey" }, { diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index 62dbb1d..a3655ab 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -271,6 +271,47 @@ export type SunriseCore = { } ] }, + { + "name": "transferGsol", + "docs": [ + "CPI request from a beam program to transfer gSol.", + "", + "Same invariants as for [minting][sunrise_core::mint_gsol()].", + "Errors if the recipient beam is not registered in the state." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "beam", + "isMut": false, + "isSigner": true + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "recipientBeam", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "removeBeam", "docs": [ @@ -1007,6 +1048,47 @@ export const IDL: SunriseCore = { } ] }, + { + "name": "transferGsol", + "docs": [ + "CPI request from a beam program to transfer gSol.", + "", + "Same invariants as for [minting][sunrise_core::mint_gsol()].", + "Errors if the recipient beam is not registered in the state." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "beam", + "isMut": false, + "isSigner": true + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "recipientBeam", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "removeBeam", "docs": [ diff --git a/packages/sdks/marinade-lp/src/index.ts b/packages/sdks/marinade-lp/src/index.ts index 740d093..d18484c 100644 --- a/packages/sdks/marinade-lp/src/index.ts +++ b/packages/sdks/marinade-lp/src/index.ts @@ -1,21 +1,23 @@ import { type AnchorProvider, Program } from "@coral-xyz/anchor"; import { + Keypair, PublicKey, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, type TransactionInstruction, - SystemProgram, - Keypair, } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { - MarinadeLpBeam, BeamInterface, deriveAuthorityAddress, + MarinadeLpBeam, sendAndConfirmChecked, } from "@sunrisestake/beams-common"; import { StateAccount } from "./state.js"; @@ -23,7 +25,7 @@ import { MARINADE_BEAM_PROGRAM_ID, MARINADE_FINANCE_PROGRAM_ID, } from "./constants.js"; -import { MarinadeLpClientParams, Utils } from "./utils.js"; +import { Balance, MarinadeLpClientParams, Utils } from "./utils.js"; import BN from "bn.js"; import { SunriseClient } from "@sunrisestake/beams-core"; @@ -59,7 +61,7 @@ export class MarinadeLpClient extends BeamInterface< provider: AnchorProvider, updateAuthority: PublicKey, sunriseState: PublicKey, - treasury: PublicKey, + msolRecipientBeam: PublicKey, msolTokenAccount: PublicKey, programId = MARINADE_BEAM_PROGRAM_ID, ): Promise { @@ -85,7 +87,7 @@ export class MarinadeLpClient extends BeamInterface< marinadeState: marinadeLpClientParams.marinade.marinadeStateAddress, sunriseState, vaultAuthorityBump, - treasury, + msolRecipientBeam, msolTokenAccount, }) .accounts({ @@ -312,21 +314,63 @@ export class MarinadeLpClient extends BeamInterface< return new Transaction().add(instruction); } - // public async extractYield(): Promise { - // const instruction = await this.program.methods - // .extractYield() - // .accounts({ - // state: this.stateAddress, - // sunriseState: this.state.sunriseState, - // systemProgram: SystemProgram.programId, - // tokenProgram: TOKEN_PROGRAM_ID, - // sysvarInstructions, - // sunriseProgram: this.sunrise.program.programId, - // }) - // .instruction(); - // - // return new Transaction().add(instruction); - // } + // Update this beam's extractable yield in the core state's epoch report + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + liqPoolMint: this.marinadeLp.marinade.lpMint.address, + liqPoolTokenVault: this.marinadeLp.beamVault, + vaultAuthority: this.vaultAuthority[0], + transferMsolTo: this.state.msolTokenAccount, + liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), + liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, + liqPoolMsolLegAuthority: + await this.marinadeLp.marinade.mSolLegAuthority(), + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + gsolMint: this.sunrise.state.gsolMint, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + + /** + * Return a transaction to extract any yield from this beam into the yield account + */ + public async extractYield(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinadeLp.marinade.mSolMint.address, + msolVault: this.state.msolTokenAccount, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), + liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, + treasuryMsolAccount: this.marinadeLp.marinade.treasuryMsolAccount, + yieldAccount: this.sunrise.state.yieldAccount, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarClock: SYSVAR_CLOCK_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + const instruction = await this.program.methods + .extractYield() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } /** * Return a transaction to redeem a ticket received from ordering a withdrawal from a marinade-lp. @@ -382,8 +426,15 @@ export class MarinadeLpClient extends BeamInterface< return new BN("" + lpMintInfo.supply); }; + public msolLegBalance = async (): Promise => { + const lpMsolLeg = this.marinadeLp.marinade.mSolLeg; + const lpMsolLegBalance = + await this.provider.connection.getTokenAccountBalance(lpMsolLeg); + return new BN(lpMsolLegBalance.value.amount); + }; + /** - * A convenience method for calculating the price of the stake-pool's token. + * A convenience method for calculating the price of the stake-pool's token in Lamports * NOTE: This might not give the current price is refresh() isn't called first. */ public poolTokenPrice = async () => { @@ -391,16 +442,41 @@ export class MarinadeLpClient extends BeamInterface< const lpSupply = lpMintInfo.supply; const solBalance = await this.poolSolBalance(); - const lpMsolLeg = this.marinadeLp.marinade.mSolLeg; - const lpMsolLegBalance = - await this.provider.connection.getTokenAccountBalance(lpMsolLeg); + const lpMsolLegBalance = await this.msolLegBalance(); const msolPrice = this.marinadeLp.marinade.mSolPrice; - const msolValue = Number(lpMsolLegBalance.value.amount) * msolPrice; - - const lpPrice = (solBalance + msolValue) / Number(lpSupply); + const msolValue = lpMsolLegBalance.toNumber() * msolPrice; - return lpPrice; + return (solBalance + msolValue) / Number(lpSupply); }; + + /** + * Calculate the value of the stake-pool's token in SOL and mSOL + * @param lpTokens + */ + public async calculateBalanceFromLpTokens(lpTokens: BN): Promise { + const totalSupply = await this.poolTokenSupply(); + const proportionOfPool = lpTokens.toNumber() / totalSupply.toNumber(); + const solBalance = await this.poolSolBalance(); + const msolLegBalance = await this.msolLegBalance(); + + const solValue = solBalance * proportionOfPool; + const msolValue = msolLegBalance.toNumber() * proportionOfPool; + + console.log("calculateBalanceFromLpTokens", { + lpTokens: lpTokens.toNumber(), + totalSupply: totalSupply.toNumber(), + proportionOfPool, + solBalance, + msolLegBalance: msolLegBalance.toNumber(), + solValue, + msolValue, + }); + + return { + lamports: new BN(solValue), + msolLamports: new BN(msolValue), + }; + } } diff --git a/packages/sdks/marinade-lp/src/state.ts b/packages/sdks/marinade-lp/src/state.ts index 8c310a6..882ab62 100644 --- a/packages/sdks/marinade-lp/src/state.ts +++ b/packages/sdks/marinade-lp/src/state.ts @@ -9,7 +9,7 @@ export class StateAccount implements BeamState { public readonly proxyState: PublicKey; public readonly sunriseState: PublicKey; public readonly vaultAuthorityBump: number; - public readonly treasury: PublicKey; + public readonly msolRecipientBeam: PublicKey; public readonly msolTokenAccount: PublicKey; private constructor( @@ -21,7 +21,7 @@ export class StateAccount implements BeamState { this.proxyState = account.marinadeState; this.sunriseState = account.sunriseState; this.vaultAuthorityBump = account.vaultAuthorityBump; - this.treasury = account.treasury; + this.msolRecipientBeam = account.msolRecipientBeam; this.msolTokenAccount = account.msolTokenAccount; } @@ -43,7 +43,7 @@ export class StateAccount implements BeamState { proxyState: this.proxyState.toBase58(), sunriseState: this.sunriseState.toBase58(), vaultAuthorityBump: this.vaultAuthorityBump.toString(), - treasury: this.treasury.toBase58(), + msolRecipientBeam: this.msolRecipientBeam.toBase58(), msolTokenAccount: this.msolTokenAccount.toBase58(), }; } diff --git a/packages/sdks/marinade-lp/src/utils.ts b/packages/sdks/marinade-lp/src/utils.ts index 6ab7f11..15f16f2 100644 --- a/packages/sdks/marinade-lp/src/utils.ts +++ b/packages/sdks/marinade-lp/src/utils.ts @@ -6,6 +6,7 @@ import { import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { AnchorProvider } from "@coral-xyz/anchor"; import { deriveAuthorityAddress } from "@sunrisestake/beams-common"; +import BN from "bn.js"; export type MarinadeLpClientParams = { /** The marinade state. */ @@ -19,6 +20,11 @@ const enum Seeds { STATE = "sunrise-marinade-lp", } +export type Balance = { + lamports: BN; + msolLamports: BN; +}; + /** A utility class containing methods for PDA-derivation. */ export class Utils { /** Derive the address of the state account for this beam. */ diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index 89b5e21..66261c5 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -518,7 +518,6 @@ export class MarinadeClient extends BeamInterface< treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, yieldAccount: this.sunrise.state.yieldAccount, sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, - sysvarClock: SYSVAR_CLOCK_PUBKEY, sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 5b080ec..918083b 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -427,6 +427,29 @@ export class SplClient extends BeamInterface { return new Transaction().add(instruction); } + /** + * Update this beam's extractable yield in the core state's epoch report + */ + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + stakePool: this.spl.stakePoolAddress, + sunriseState: this.state.sunriseState, + poolMint: this.spl.stakePoolState.poolMint, + vaultAuthority: this.vaultAuthority[0], + poolTokenVault: this.spl.beamVault, + gsolMint: this.sunrise.state.gsolMint, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + /** * Return a transaction to extract any yield from this beam into the yield account */ diff --git a/packages/tests/src/functional/beams/marinade-lp.test.ts b/packages/tests/src/functional/beams/marinade-lp.test.ts index 220a179..f886956 100644 --- a/packages/tests/src/functional/beams/marinade-lp.test.ts +++ b/packages/tests/src/functional/beams/marinade-lp.test.ts @@ -8,7 +8,7 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { - createTokenAccount, + expectSolBalance, expectTokenBalance, fund, logAtLevel, @@ -17,12 +17,13 @@ import { tokenAccountBalance, } from "../../utils.js"; import { expect } from "chai"; -import { MSOL_MINT } from "../consts.js"; +import { MarinadeClient } from "@sunrisestake/beams-marinade-sp"; describe("Marinade liquidity pool beam", () => { let coreClient: SunriseClient; let beamClient: MarinadeLpClient; let stakerGsolBalance: BN; + let netExtractableYield: BN; let sunriseStateAddress: PublicKey; let vaultBalance: BN; @@ -30,6 +31,7 @@ describe("Marinade liquidity pool beam", () => { const depositAmount = 10 * LAMPORTS_PER_SOL; const failedDepositAmount = 5 * LAMPORTS_PER_SOL; const withdrawalAmount = 5 * LAMPORTS_PER_SOL; + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -57,24 +59,24 @@ describe("Marinade liquidity pool beam", () => { }); it("can initialize a state", async () => { - // create an MSol token account for the beam. - // NOTE - when combined with the marinade-sp beam, this should be the msol token account - // associated with the marinade stake pool. - const msolTokenAccount = await createTokenAccount( + // Register the SP beam first. + // Note - the Marinade LP beam cannot exist without the Marinade SP beam (or some other beam + // that stores msol) because it needs somewhere to send the "excess" msol whenever someone withdraws + // gsol from the pool. + const marinadeSpBeamClient = await MarinadeClient.initialize( provider, + provider.publicKey, sunriseStateAddress, - MSOL_MINT, ); await SunriseClient.get(provider, sunriseStateAddress); - const treasury = Keypair.generate(); beamClient = await MarinadeLpClient.initialize( provider, provider.publicKey, sunriseStateAddress, - treasury.publicKey, - msolTokenAccount, + marinadeSpBeamClient.stateAddress, + marinadeSpBeamClient.marinade.beamMsolVault, ); const info = beamClient.state.pretty(); @@ -94,13 +96,14 @@ describe("Marinade liquidity pool beam", () => { }); it("can update a state", async () => { - const newTreasury = Keypair.generate(); + const oldTokenAccount = beamClient.state.msolTokenAccount; + const newTokenAccount = Keypair.generate().publicKey; const updateParams = { updateAuthority: beamClient.state.updateAuthority, sunriseState: beamClient.state.sunriseState, vaultAuthorityBump: beamClient.state.vaultAuthorityBump, - treasury: newTreasury.publicKey, - msolTokenAccount: beamClient.state.msolTokenAccount, + msolRecipientBeam: beamClient.state.msolRecipientBeam, + msolTokenAccount: newTokenAccount, marinadeState: beamClient.state.proxyState, }; await sendAndConfirmTransaction( @@ -110,8 +113,18 @@ describe("Marinade liquidity pool beam", () => { ); beamClient = await beamClient.refresh(); - expect(beamClient.state.treasury.toBase58()).to.equal( - newTreasury.publicKey.toBase58(), + expect(beamClient.state.msolTokenAccount.toBase58()).to.equal( + newTokenAccount.toBase58(), + ); + + // change it back :) + await sendAndConfirmTransaction( + provider, + await beamClient.update(provider.publicKey, { + ...updateParams, + msolTokenAccount: oldTokenAccount, + }), + [], ); }); @@ -172,6 +185,19 @@ describe("Marinade liquidity pool beam", () => { ); }); + it("can update the epoch report with zero extractable yield", async () => { + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(0); + }); + it("can't deposit due to exceeding allocation", async () => { const shouldFail = sendAndConfirmTransaction( stakerIdentity, @@ -222,6 +248,19 @@ describe("Marinade liquidity pool beam", () => { ); }); + it("can update the epoch report with zero extractable yield", async () => { + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(0); + }); + it("can burn gsol", async () => { // burn some gsol to simulate the creation of yield const burnAmount = new BN(1 * LAMPORTS_PER_SOL); @@ -239,14 +278,63 @@ describe("Marinade liquidity pool beam", () => { ); }); - // it("can extract yield", async () => { - // // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) - // - // await sendAndConfirmTransaction( - // // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test - // // to show that it doesn't have to be an admin - // stakerIdentity, - // await beamClient.extractYield(), - // ) - // }); + it("can update the epoch report with the extractable yield", async () => { + // in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens + // when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) + // the SOL is sent to the yield account, + // and the mSOL is sent to the beam's mSOL token account, which is typically + // the Marinade-SP's beam vault. + // This results in less extractable yield for this beam, and more for the Marinade-SP beam. + // (However, in reality, this beam should rarely be extracted from, as it is + // included as a buffer to allow for fee-less gSOL withdrawals) + const expectedFee = burnAmount.toNumber() * 0.003; + const effectiveBurnedAmount = burnAmount.subn(expectedFee); + const lpTokenValue = + effectiveBurnedAmount.toNumber() / (await beamClient.poolTokenPrice()); + const lpBalance = await beamClient.calculateBalanceFromLpTokens( + new BN(lpTokenValue), + ); + netExtractableYield = lpBalance.lamports; + console.log("burnAmount", burnAmount.toNumber()); + console.log("expectedFee", expectedFee); + console.log("effectiveBurnedAmount", effectiveBurnedAmount.toNumber()); + console.log("poolTokenPrice", await beamClient.poolTokenPrice()); + console.log("lpTokenValue", lpTokenValue); + console.log("lpBalance", { + lamports: lpBalance.lamports.toNumber(), + msolLamports: lpBalance.msolLamports.toNumber(), + }); + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(netExtractableYield.toNumber()); + }); + + it("can extract yield into a stake account", async () => { + // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) + // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. + + await sendAndConfirmTransaction( + // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.extractYield(), + ); + + await expectSolBalance( + beamClient.provider, + beamClient.sunrise.state.yieldAccount, + netExtractableYield, // calculated in the previous test + 1, + ); + }); }); diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 00d52cb..7a19bac 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -12,7 +12,6 @@ import { } from "@solana/web3.js"; import BN from "bn.js"; import { - createTokenAccount, expectSolBalance, expectStakerSolBalance, expectTokenBalance, @@ -70,8 +69,6 @@ describe("Marinade stake pool beam", () => { }); it("can initialize a state", async () => { - // create an MSol token account for the beam. - await createTokenAccount(provider, sunriseStateAddress, MSOL_MINT); beamClient = await MarinadeClient.initialize( provider, provider.publicKey, @@ -315,7 +312,7 @@ describe("Marinade stake pool beam", () => { ); }); - it("can update the epoch report", async () => { + it("can update the epoch report with the extractable yield", async () => { // we burned `burnAmount` gsol, so we should be able to extract `burnAmount` - estimated fee const expectedFee = burnAmount.toNumber() * 0.003; extractableYield = burnAmount.subn(expectedFee); diff --git a/packages/tests/src/functional/beams/spl-stake-pool.test.ts b/packages/tests/src/functional/beams/spl-stake-pool.test.ts index b7d00e0..4c6cead 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -24,6 +24,7 @@ describe("SPL stake pool beam", () => { let vaultStakePoolSolBalance: BN; let stakerGsolBalance: BN = new BN(0); let sunriseStateAddress: PublicKey; + let extractableYield: BN; const stakePool: PublicKey = SPL_STAKE_POOL; @@ -202,6 +203,27 @@ describe("SPL stake pool beam", () => { ); }); + it("can update the epoch report with the extractable yield", async () => { + // we burned `burnAmount` gsol, so we should be able to extract `burnAmount` - estimated fee + const expectedFee = burnAmount + .mul(beamClient.spl.stakePoolState.stakeWithdrawalFee.numerator) + .div(beamClient.spl.stakePoolState.stakeWithdrawalFee.denominator); + extractableYield = burnAmount.sub(expectedFee); + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(extractableYield.toNumber()); + }); + it("can extract yield into a stake account", async () => { // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) await sendAndConfirmTransaction( @@ -211,16 +233,10 @@ describe("SPL stake pool beam", () => { await beamClient.extractYield(), ); - // we burned `burnAmount` gsol, so we should have `burnAmount` in the yield account - const expectedFee = burnAmount - .mul(beamClient.spl.stakePoolState.stakeWithdrawalFee.numerator) - .div(beamClient.spl.stakePoolState.stakeWithdrawalFee.denominator); - const expectedExtractedYield = burnAmount.sub(expectedFee); - await expectSolBalance( beamClient.provider, beamClient.sunrise.state.yieldAccount, - expectedExtractedYield, + extractableYield, // the calculation appears to be slightly inaccurate at present, but in our favour, // so we can leave this as a low priority TODO to improve the accuracy 3000, diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 989035e..5906651 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -715,8 +715,6 @@ pub struct ExtractYield<'info> { /// CHECK: Checked by Marinade CPI. pub treasury_msol_account: UncheckedAccount<'info>, - pub sysvar_clock: Sysvar<'info, Clock>, - /// CHECK: Checked by Sunrise CPI. pub sysvar_instructions: UncheckedAccount<'info>, diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index c657a10..1b1b04d 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -4,8 +4,11 @@ use crate::constants::STATE; use sunrise_core as sunrise_core_cpi; use sunrise_core::cpi::accounts::{ExtractYield, UpdateEpochReport}; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, MintGsol}, - burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + accounts::{BurnGsol, MintGsol, TransferGsol}, + burn_gsol as cpi_burn_gsol, + mint_gsol as cpi_mint_gsol, + transfer_gsol as cpi_transfer_gsol, + extract_yield as cpi_extract_yield, update_epoch_report as cpi_update_epoch_report, }; @@ -26,6 +29,20 @@ pub fn mint_gsol<'a>( ) } +impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { + fn from(accounts: &crate::Deposit<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), + mint_gsol_to: accounts.mint_gsol_to.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} + pub fn burn_gsol<'a>( accounts: impl Into>, cpi_program: AccountInfo<'a>, @@ -43,27 +60,27 @@ pub fn burn_gsol<'a>( ) } -impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { - fn from(accounts: &crate::Deposit<'a>) -> Self { +impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { + fn from(accounts: &crate::Withdraw<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), - mint_gsol_to: accounts.mint_gsol_to.to_account_info(), + burn_gsol_from_owner: accounts.withdrawer.to_account_info(), + burn_gsol_from: accounts.gsol_token_account.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } } -impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { - fn from(accounts: &crate::Withdraw<'a>) -> Self { +impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { + fn from(accounts: &crate::Burn<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - burn_gsol_from_owner: accounts.withdrawer.to_account_info(), + burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), @@ -71,16 +88,32 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { } } -impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { - fn from(accounts: &crate::Burn<'a>) -> Self { +pub fn transfer_gsol<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + recipient_beam: Pubkey, + lamports: u64, +) -> Result<()> { + let accounts: TransferGsol<'a> = accounts.into(); + let seeds = [crate::constants::STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_transfer_gsol( + CpiContext::new(cpi_program, accounts).with_signer(signer), + recipient_beam, + lamports, + ) +} + +impl<'a> From<&crate::Withdraw<'a>> for TransferGsol<'a> { + fn from(accounts: &crate::Withdraw<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - burn_gsol_from_owner: accounts.burner.to_account_info(), - burn_gsol_from: accounts.gsol_token_account.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), - token_program: accounts.token_program.to_account_info(), } } } diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index 8c8a909..27d66eb 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -7,16 +7,15 @@ use anchor_spl::associated_token::{AssociatedToken, Create}; use anchor_spl::token::{Mint, Token, TokenAccount}; use marinade_cpi::State as MarinadeState; use std::ops::Deref; - -mod cpi_interface; -mod state; -mod system; - use cpi_interface::marinade_lp as marinade_lp_interface; use cpi_interface::sunrise as sunrise_interface; use state::{State, StateEntry}; use system::utils; +mod cpi_interface; +mod state; +mod system; + // TODO: Use actual CPI crate. use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; @@ -32,9 +31,9 @@ mod constants { #[program] pub mod marinade_lp_beam { + use marinade_common::calc_lamports_from_msol_amount; use super::*; use crate::cpi_interface::marinade_lp; - use crate::system::utils::calc_liq_pool_tokens_from_lamports; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -75,21 +74,28 @@ pub mod marinade_lp_beam { } pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { - // Calculate the number of liq_pool tokens `lamports` is worth. - let liq_pool_tokens = utils::calc_liq_pool_tokens_from_lamports( + // Calculate the number of liq_pool tokens that would be needed to withdraw `lamports` + let liq_pool_balance = utils::calculate_liq_pool_balance_required_to_withdraw_lamports( &ctx.accounts.marinade_state, &ctx.accounts.liq_pool_mint, &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, lamports, )?; // CPI: Remove liquidity from Marinade liq_pool. The liq_pool tokens vault owned by // this vault is the source burning lp tokens. + // The result is a combination of SOL and mSOL. + // The SOL goes to the withdrawer, and the mSOL goes to the designated mSOL token account. + // which is typically the Marinade-SP's beam vault. + // NOTE: This results in an asymmetry in the amount of gSOL each beam is responsible for + // The lamport amount is burned, and the mSOL amount has been effectively moved from + // the beam to the Marinade-SP (or whichever beam owns the msol token account) let accounts = ctx.accounts.deref().into(); marinade_lp::remove_liquidity( &ctx.accounts.marinade_program, &ctx.accounts.state, accounts, - liq_pool_tokens, + liq_pool_balance.liq_pool_token, )?; let state_bump = ctx.bumps.state; @@ -102,6 +108,19 @@ pub mod marinade_lp_beam { lamports, )?; + let lamport_value_of_msol = calc_lamports_from_msol_amount( + &ctx.accounts.marinade_state, + liq_pool_balance.msol + ).map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + sunrise_interface::transfer_gsol( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + ctx.accounts.state.msol_recipient_beam, + lamport_value_of_msol, + )?; + Ok(()) } @@ -132,7 +151,7 @@ pub mod marinade_lp_beam { } pub fn extract_yield(ctx: Context) -> Result<()> { - let yield_lamports = utils::calculate_extractable_yield( + let yield_balance = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, @@ -142,13 +161,6 @@ pub mod marinade_lp_beam { &ctx.accounts.liq_pool_msol_leg, )?; - let yield_liq_pool_tokens = calc_liq_pool_tokens_from_lamports( - &ctx.accounts.marinade_state, - &ctx.accounts.liq_pool_mint, - &ctx.accounts.liq_pool_sol_leg_pda, - yield_lamports, - )?; - let yield_account_balance_before = ctx.accounts.yield_account.lamports(); let accounts = ctx.accounts.deref().into(); @@ -156,7 +168,7 @@ pub mod marinade_lp_beam { &ctx.accounts.marinade_program, &ctx.accounts.state, accounts, - yield_liq_pool_tokens, + yield_balance.liq_pool_token, )?; let yield_account_balance_after = ctx.accounts.yield_account.lamports(); @@ -179,7 +191,7 @@ pub mod marinade_lp_beam { } pub fn update_epoch_report(ctx: Context) -> Result<()> { - let yield_lamports = utils::calculate_extractable_yield( + let yield_balance = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, @@ -189,9 +201,18 @@ pub mod marinade_lp_beam { &ctx.accounts.liq_pool_msol_leg, )?; - // Reduce by fee - // TODO can we do better than an estimate? - let extractable_lamports = (yield_lamports as f64) * 0.997; // estimated 0.3% unstake fee + // in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens + // when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) + // the SOL is sent to the yield account, + // and the mSOL is sent to the beam's mSOL token account, which is typically + // the Marinade-SP's beam vault. + // This results in less extractable yield for this beam, and more for the Marinade-SP beam. + // (However, in reality, this beam should rarely be extracted from, as it is + // included as a buffer to allow for fee-less gSOL withdrawals) + // Subtract fee TODO can we do better than an estimate? + let extractable_lamports = (yield_balance.lamports as f64) * 0.997; // estimated 0.3% unstake fee + msg!("yield_balance: {:?}", yield_balance); + msg!("Extractable yield: {}", extractable_lamports); // CPI: update the epoch report with the extracted yield. let state_bump = ctx.bumps.state; @@ -364,7 +385,7 @@ pub struct Withdraw<'info> { pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. - pub liq_pool_msol_leg: UncheckedAccount<'info>, + pub liq_pool_msol_leg: Box>, #[account(mut)] /// CHECK: Checked by Marinade CPI. pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, @@ -528,14 +549,11 @@ pub struct UpdateEpochReport<'info> { #[account(mut)] /// CHECK: Checked by Marinade CPI. pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, - /// CHECK: Checked by Marinade CPI. - pub system_program: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - pub marinade_program: Program<'info, Marinade>, } #[derive(Accounts)] diff --git a/programs/marinade-lp-beam/src/state.rs b/programs/marinade-lp-beam/src/state.rs index 3b9f487..a6ed465 100644 --- a/programs/marinade-lp-beam/src/state.rs +++ b/programs/marinade-lp-beam/src/state.rs @@ -16,8 +16,9 @@ pub struct State { /// that holds pool tokens(both liq_pool and marinade stake pool). pub vault_authority_bump: u8, - /// This state's SOL vault. - pub treasury: Pubkey, + /// The beam address of the recipient of msol when withdrawing liquidity. + /// Typically the marinade-sp beam + pub msol_recipient_beam: Pubkey, /// The token-account that receives msol when withdrawing liquidity. pub msol_token_account: Pubkey, @@ -35,7 +36,7 @@ impl State { 32 + /*marinade_state*/ 32 + /*sunrise_state*/ 1 + /*vault_authority_bump*/ - 32 + /*treasury*/ + 32 + /*msol_recipient_beam*/ 32; /*msol_token_account*/ } @@ -48,7 +49,7 @@ pub struct StateEntry { pub marinade_state: Pubkey, pub sunrise_state: Pubkey, pub vault_authority_bump: u8, - pub treasury: Pubkey, + pub msol_recipient_beam: Pubkey, pub msol_token_account: Pubkey, } @@ -59,7 +60,7 @@ impl From for State { marinade_state: se.marinade_state, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - treasury: se.treasury, + msol_recipient_beam: se.msol_recipient_beam, msol_token_account: se.msol_token_account, } } diff --git a/programs/marinade-lp-beam/src/system/balance.rs b/programs/marinade-lp-beam/src/system/balance.rs index 07d6dbc..3dfe761 100644 --- a/programs/marinade-lp-beam/src/system/balance.rs +++ b/programs/marinade-lp-beam/src/system/balance.rs @@ -3,7 +3,7 @@ use marinade_common::{calc_lamports_from_msol_amount, proportional}; use marinade_cpi::State as MarinadeState; #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(super) struct LiquidityPoolBalance { +pub struct LiquidityPoolBalance { pub lamports: u64, pub msol: u64, pub liq_pool_token: u64, @@ -32,7 +32,8 @@ impl LiquidityPoolBalance { // The value of both legs of the liquidity pool balance in SOL pub fn sol_value(&self, marinade_state: &MarinadeState) -> u64 { let lamports = self.lamports; - let msol = calc_lamports_from_msol_amount(marinade_state, self.msol).unwrap(); + let msol = calc_lamports_from_msol_amount(marinade_state, self.msol) + .unwrap(); lamports.checked_add(msol).expect("sol_value") } @@ -72,4 +73,24 @@ impl LiquidityPoolBalance { liq_pool_token: new_liq_pool_token, }) } + + pub fn checked_sub(&self, other: Self) -> Result { + let new_lamports = self + .lamports + .checked_sub(other.lamports) + .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let new_liq_pool_token = self + .liq_pool_token + .checked_sub(other.liq_pool_token) + .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let new_msol = self + .msol + .checked_sub(other.msol) + .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + Ok(Self { + lamports: new_lamports, + msol: new_msol, + liq_pool_token: new_liq_pool_token, + }) + } } diff --git a/programs/marinade-lp-beam/src/system/utils.rs b/programs/marinade-lp-beam/src/system/utils.rs index 9cc2a78..e544486 100644 --- a/programs/marinade-lp-beam/src/system/utils.rs +++ b/programs/marinade-lp-beam/src/system/utils.rs @@ -2,7 +2,7 @@ use super::balance::LiquidityPoolBalance; use crate::state::State; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; -use marinade_common::proportional; +use marinade_common::{calc_lamports_from_msol_amount, proportional}; use marinade_cpi::State as MarinadeState; use sunrise_core::BeamError; @@ -15,30 +15,81 @@ pub fn calculate_extractable_yield( liq_pool_token_account: &TokenAccount, liq_pool_sol_leg_pda: &AccountInfo, liq_pool_msol_leg: &TokenAccount, -) -> Result { - let staked_value = current_liq_pool_balance( +) -> Result { + // the liquidity pool balance owned by the beam (LP tokens, SOL and mSOL) + let staked_balance = current_liq_pool_balance( marinade_state, liq_pool_mint, liq_pool_token_account, liq_pool_sol_leg_pda, liq_pool_msol_leg, - )? - .sol_value(marinade_state); + )?; + + // the amount of SOL that this beam is responsible for. + // The value of the liquidity pool is at least this high. However, the lp value is split across + // SOL and mSOL let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; let staked_sol = details.partial_gsol_supply; - Ok(staked_value.saturating_sub(staked_sol)) + + // the lp balance that is required to cover the staked SOL. This is a combination of SOL and mSOL. + let required_lp_tokens_to_cover_staked_sol = calculate_liq_pool_token_value_of_lamports( + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + staked_sol, + )?; + + msg!("staked_balance: {:?}", staked_balance); + msg!("staked_sol: {:?}", staked_sol); + msg!("required_lp_tokens_to_cover_staked_sol: {:?}", required_lp_tokens_to_cover_staked_sol); + + let required_liq_pool_balance = liq_pool_balance_for_tokens( + required_lp_tokens_to_cover_staked_sol, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + )?; + msg!("required_liq_pool_balance: {:?}", required_liq_pool_balance); + + // return the difference between the staked balance and the required balance + let diff = staked_balance.checked_sub(required_liq_pool_balance)?; + + msg!("diff: {:?}", diff); + Ok(diff) } // Prevent the compiler from enlarging the stack and potentially triggering an Access violation #[inline(never)] +/// Returns the current liquidity pool balance owned by the beam pub(super) fn current_liq_pool_balance( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_token_account: &TokenAccount, liq_pool_sol_leg_pda: &AccountInfo, liq_pool_msol_leg: &TokenAccount, +) -> Result { + liq_pool_balance_for_tokens( + liq_pool_token_account.amount, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + ) +} + +// Prevent the compiler from enlarging the stack and potentially triggering an Access violation +#[inline(never)] +/// Returns the liquidity pool balance for a given amount of lp tokens +pub(super) fn liq_pool_balance_for_tokens( + tokens: u64, + marinade_state: &MarinadeState, + liq_pool_mint: &Mint, + liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, ) -> Result { //compute current liq-pool total value let total_balance = total_liq_pool( @@ -50,22 +101,42 @@ pub(super) fn current_liq_pool_balance( // The SOL amount held by sunrise in the liquidity pool is the total value of the pool in SOL // multiplied by the proportion of the pool owned by this SunshineStake instance - let sunrise_liq_pool_balance = total_balance.value_of(liq_pool_token_account.amount)?; + let sunrise_liq_pool_balance = total_balance.value_of(tokens)?; msg!("Total LP: {:?}", total_balance); - msg!("Sunrise LP: {:?}", sunrise_liq_pool_balance); + msg!("LP for token: {:?}", sunrise_liq_pool_balance); msg!( "Total LP value: {:?}", total_balance.sol_value(marinade_state) ); msg!( - "Sunrise LP value: {:?}", + "LP value: {:?}", sunrise_liq_pool_balance.sol_value(marinade_state) ); Ok(sunrise_liq_pool_balance) } +pub fn calculate_liq_pool_token_value_of_lamports( + marinade_state: &MarinadeState, + liq_pool_mint: &Mint, + liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, + lamports: u64, +) -> Result { + let total_lamports = liq_pool_sol_leg_pda + .lamports() + .checked_sub(marinade_state.rent_exempt_for_token_acc) + .unwrap(); + let total_msol = liq_pool_msol_leg.amount; + let lamports_value_of_msol = calc_lamports_from_msol_amount(marinade_state, total_msol) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let total_value_of_pool = total_lamports.checked_add(lamports_value_of_msol).unwrap(); + + proportional(liq_pool_mint.supply, lamports, total_value_of_pool) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) +} + fn total_liq_pool( marinade_state: &MarinadeState, liq_pool_mint: &Mint, @@ -84,18 +155,27 @@ fn total_liq_pool( ) } -pub fn calc_liq_pool_tokens_from_lamports( +pub fn calculate_liq_pool_balance_required_to_withdraw_lamports( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, lamports: u64, -) -> Result { +) -> Result { let liq_pool_lamports = liq_pool_sol_leg_pda .lamports() .checked_sub(marinade_state.rent_exempt_for_token_acc) .unwrap(); let liq_pool_mint_supply = liq_pool_mint.supply; - proportional(liq_pool_mint_supply, lamports, liq_pool_lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) + let liq_pool_tokens = proportional(liq_pool_mint_supply, lamports, liq_pool_lamports) + .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + + liq_pool_balance_for_tokens( + liq_pool_tokens, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg + ) } diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index 54722a9..e52693f 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -30,6 +30,7 @@ declare_id!("EUZfY4LePXSZVMvRuiVzbxazw9yBDYU99DpGJKCthxbS"); pub mod spl_beam { use super::*; use crate::cpi_interface::stake_account::claim_stake_account; + use crate::utils::proportional; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -168,13 +169,21 @@ pub mod spl_beam { pub fn update_epoch_report(ctx: Context) -> Result<()> { // Calculate how much yield can be extracted from the pool. - let extractable_yield = utils::calculate_extractable_yield( + let gross_extractable_yield = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.stake_pool, &ctx.accounts.pool_token_vault, )?; + // Reduce by fee + let fee = proportional( + gross_extractable_yield, + ctx.accounts.stake_pool.stake_withdrawal_fee.numerator, + ctx.accounts.stake_pool.stake_withdrawal_fee.denominator, + )?; + let net_extractable_yield = gross_extractable_yield.saturating_sub(fee); + // CPI: update the epoch report with the extracted yield. let state_bump = ctx.bumps.state; sunrise_interface::update_epoch_report( @@ -183,7 +192,7 @@ pub mod spl_beam { ctx.accounts.sunrise_state.key(), ctx.accounts.stake_pool.key(), state_bump, - extractable_yield, + net_extractable_yield, )?; Ok(()) diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index 39286e1..fa058c1 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -2,7 +2,6 @@ use crate::{system, token, utils, BeamError, BurnGsol}; use anchor_lang::prelude::*; pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { - let amount = amount_in_lamports; let state = &mut ctx.accounts.state; // Check that the requesting program is valid. @@ -15,18 +14,18 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { .ok_or(BeamError::UnidentifiedBeam)?; // Can't burn more gsol than this beam is responsible for. - if details.partial_gsol_supply < amount { + if details.partial_gsol_supply < amount_in_lamports { msg!( "Beam supply {}, requested burn {}", details.partial_gsol_supply, - amount + amount_in_lamports ); return Err(BeamError::BurnWindowExceeded.into()); } - details.partial_gsol_supply = details.partial_gsol_supply.checked_sub(amount).unwrap(); + details.partial_gsol_supply = details.partial_gsol_supply.checked_sub(amount_in_lamports).unwrap(); token::burn( - amount, + amount_in_lamports, &ctx.accounts.gsol_mint.to_account_info(), &ctx.accounts.burn_gsol_from_owner.to_account_info(), &ctx.accounts.burn_gsol_from.to_account_info(), diff --git a/programs/sunrise-core/src/instructions/mod.rs b/programs/sunrise-core/src/instructions/mod.rs index 5965923..f13acd1 100644 --- a/programs/sunrise-core/src/instructions/mod.rs +++ b/programs/sunrise-core/src/instructions/mod.rs @@ -9,6 +9,7 @@ pub mod resize_allocations; pub mod update_allocations; pub mod update_epoch_report; pub mod update_state; +pub mod transfer_gsol; pub use burn_gsol::*; pub use export_mint_authority::*; @@ -21,3 +22,4 @@ pub use resize_allocations::*; pub use update_allocations::*; pub use update_epoch_report::*; pub use update_state::*; +pub use transfer_gsol::*; diff --git a/programs/sunrise-core/src/instructions/transfer_gsol.rs b/programs/sunrise-core/src/instructions/transfer_gsol.rs new file mode 100644 index 0000000..1f34acd --- /dev/null +++ b/programs/sunrise-core/src/instructions/transfer_gsol.rs @@ -0,0 +1,34 @@ +use crate::{system, utils, BeamError, TransferGsol}; +use anchor_lang::prelude::*; + +pub fn handler(ctx: Context, recipient_beam: Pubkey, amount_in_lamports: u64) -> Result<()> { + let amount = amount_in_lamports; + let state = &mut ctx.accounts.state; + + // Check that the requesting program is valid. + let cpi_program = + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + + let source_beam_details = state + .get_mut_beam_details(&ctx.accounts.beam.key()) + .ok_or(BeamError::UnidentifiedBeam)?; + + // Can't transfer more gsol than this beam is responsible for. + if source_beam_details.partial_gsol_supply < amount { + msg!( + "Beam supply {}, requested burn {}", + source_beam_details.partial_gsol_supply, + amount + ); + return Err(BeamError::BurnWindowExceeded.into()); + } + source_beam_details.partial_gsol_supply = source_beam_details.partial_gsol_supply.checked_sub(amount).unwrap(); + + let target_beam_details = state + .get_mut_beam_details(&recipient_beam) + .ok_or(BeamError::UnidentifiedBeam)?; + target_beam_details.partial_gsol_supply = target_beam_details.partial_gsol_supply.checked_add(amount).unwrap(); + + Ok(()) +} diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index 8d85462..12b79ae 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -74,6 +74,14 @@ pub mod sunrise_core { burn_gsol::handler(ctx, amount) } + /// CPI request from a beam program to transfer gSol. + /// + /// Same invariants as for [minting][sunrise_core::mint_gsol()]. + /// Errors if the recipient beam is not registered in the state. + pub fn transfer_gsol(ctx: Context, recipient_beam: Pubkey, amount: u64) -> Result<()> { + transfer_gsol::handler(ctx, recipient_beam, amount) + } + /// Removes a beam from the state. /// /// Errors if the beam's allocation is not set to zero. @@ -194,6 +202,28 @@ pub struct BurnGsol<'info> { pub token_program: Program<'info, Token>, } +/// Moves the supply of gsol from one beam to another. +/// gsol is not transferred between accounts in this instruction. +/// Used if a beam moves underlying assets from itself to another, thus +/// implying a move in the amount of gsol it's responsible for. +#[derive(Accounts)] +pub struct TransferGsol<'info> { + #[account( + mut, + has_one = gsol_mint, + )] + pub state: Account<'info, State>, + + pub beam: Signer<'info>, + + #[account(mut)] + pub gsol_mint: Account<'info, Mint>, + + /// CHECK: Verified Instructions Sysvar. + #[account(address = sysvar::instructions::ID)] + pub sysvar_instructions: UncheckedAccount<'info>, +} + #[derive(Accounts)] #[instruction(amount_in_lamports: u64)] pub struct MintGsol<'info> { diff --git a/yarn.lock b/yarn.lock index 33456a0..6cd8074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2305,10 +2305,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^0.14.1: - version "0.14.4" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.4.tgz#2454969b466a5f75067d0f2fa0d7426d32881b20" - integrity sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== dependencies: ansi-sequence-parser "^1.1.0" jsonc-parser "^3.2.0" @@ -2567,15 +2567,15 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typedoc@^0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.1.tgz#50de2d8fb93623fbfb59e2fa6407ff40e3d3f438" - integrity sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA== +typedoc@^0.25.8: + version "0.25.8" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.8.tgz#7d0e1bf12d23bf1c459fd4893c82cb855911ff12" + integrity sha512-mh8oLW66nwmeB9uTa0Bdcjfis+48bAjSH3uqdzSuSawfduROQLlXw//WSNZLYDdhmMVB7YcYZicq6e8T0d271A== dependencies: lunr "^2.3.9" marked "^4.3.0" minimatch "^9.0.3" - shiki "^0.14.1" + shiki "^0.14.7" typescript@^5.3.2: version "5.3.2" From 0da9b702525d8a73a8888ed323aea8e344bbc54a Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 29 Feb 2024 10:57:48 +0100 Subject: [PATCH 6/8] Marinade LP: Extract yield --- Cargo.lock | 1 + lib/marinade-common/Cargo.toml | 3 +- lib/marinade-common/src/lib.rs | 72 ++++++++++++------- .../sdks/common/src/types/marinade_beam.ts | 22 ++---- .../sdks/common/src/types/marinade_lp_beam.ts | 10 +++ packages/sdks/marinade-lp/src/index.ts | 25 +++---- .../src/functional/beams/marinade-lp.test.ts | 49 +++++++------ .../src/functional/beams/marinade-sp.test.ts | 2 - packages/tests/src/functional/consts.ts | 7 +- programs/marinade-beam/src/lib.rs | 11 +-- programs/marinade-beam/src/system/utils.rs | 3 +- .../src/cpi_interface/sunrise.rs | 7 +- programs/marinade-lp-beam/src/lib.rs | 68 ++++++++++-------- .../marinade-lp-beam/src/system/balance.rs | 64 +++++++---------- programs/marinade-lp-beam/src/system/utils.rs | 55 +++++++++----- .../src/instructions/burn_gsol.rs | 5 +- programs/sunrise-core/src/instructions/mod.rs | 4 +- .../src/instructions/transfer_gsol.rs | 16 ++++- programs/sunrise-core/src/lib.rs | 6 +- programs/sunrise-core/src/state.rs | 4 ++ 20 files changed, 244 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 821c5bd..fa87a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2283,6 +2283,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "marinade-cpi", + "num-traits", ] [[package]] diff --git a/lib/marinade-common/Cargo.toml b/lib/marinade-common/Cargo.toml index 3f0265d..51d262a 100644 --- a/lib/marinade-common/Cargo.toml +++ b/lib/marinade-common/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" [dependencies] anchor-lang = '0.29.0' -marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } \ No newline at end of file +marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } +num-traits = "0.2.17" \ No newline at end of file diff --git a/lib/marinade-common/src/lib.rs b/lib/marinade-common/src/lib.rs index 5966ef0..3633a1d 100644 --- a/lib/marinade-common/src/lib.rs +++ b/lib/marinade-common/src/lib.rs @@ -1,46 +1,68 @@ pub mod vault_authority_seed; -use anchor_lang::prelude::*; use marinade_cpi::state::State as MarinadeState; -use std::num::TryFromIntError; +use num_traits::{NumCast, PrimInt}; +use std::fmt::Debug; +use std::ops::{Div, Mul}; /// calculate amount*numerator/denominator /// as value = shares * share_price where share_price=total_value/total_shares /// or shares = amount_value / share_price where share_price=total_value/total_shares /// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value -pub fn proportional( - amount: u64, - numerator: u64, - denominator: u64, -) -> std::result::Result { - if denominator == 0 { - return Ok(amount); +pub fn proportional(amount: T, numerator: T, denominator: T) -> T +where + T: PrimInt + Mul + Div + TryInto, + >::Error: Debug, +{ + proportional_with_rounding(amount, numerator, denominator, RoundingMode::Down) +} + +pub enum RoundingMode { + Up, + Down, +} + +pub fn proportional_with_rounding( + amount: T, + numerator: T, + denominator: T, + rounding_mode: RoundingMode, +) -> T +where + T: PrimInt + Mul + Div + TryInto, + >::Error: Debug, +{ + if denominator == T::zero() { + return amount; + } + + let amount_i128: i128 = amount.try_into().unwrap(); + let numerator_i128: i128 = numerator.try_into().unwrap(); + let denominator_i128: i128 = denominator.try_into().unwrap(); + + match rounding_mode { + RoundingMode::Up => { + // Round up by adding (denominator - 1) before dividing + let result = (amount_i128 * numerator_i128 + (denominator_i128 - 1)) / denominator_i128; + ::from(result).unwrap() + } + RoundingMode::Down => { + // Default behavior (round down) + let result = amount_i128 * numerator_i128 / denominator_i128; + ::from(result).unwrap() + } } - u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) } // All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 -pub fn calc_msol_from_lamports( - marinade_state: &MarinadeState, - stake_lamports: u64, -) -> std::result::Result { - msg!("calc_msol_from_lamports"); - msg!("stake_lamports: {}", stake_lamports); - msg!("marinade_state.msol_supply: {}", marinade_state.msol_supply); - msg!( - "total_virtual_staked_lamports: {}", - total_virtual_staked_lamports(marinade_state) - ); +pub fn calc_msol_from_lamports(marinade_state: &MarinadeState, stake_lamports: u64) -> u64 { proportional( stake_lamports, marinade_state.msol_supply, total_virtual_staked_lamports(marinade_state), ) } -pub fn calc_lamports_from_msol_amount( - marinade_state: &MarinadeState, - msol_amount: u64, -) -> std::result::Result { +pub fn calc_lamports_from_msol_amount(marinade_state: &MarinadeState, msol_amount: u64) -> u64 { proportional( msol_amount, total_virtual_staked_lamports(marinade_state), diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 77fa7bf..992e909 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -884,26 +884,21 @@ export type MarinadeBeam = { }, { "code": 6001, - "name": "CalculationFailure", - "msg": "An error occurred during calculation" - }, - { - "code": 6002, "name": "InvalidEpochReportAccount", "msg": "The epoch report account has not been updated to the current epoch yet" }, { - "code": 6003, + "code": 6002, "name": "RemainingUnclaimableTicketAmount", "msg": "The total ordered ticket amount exceeds the amount in all found tickets" }, { - "code": 6004, + "code": 6003, "name": "DelayedUnstakeTicketsNotYetClaimable", "msg": "Delayed unstake tickets for the current epoch can not yet be claimed" }, { - "code": 6005, + "code": 6004, "name": "TooManyTicketsClaimed", "msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report" } @@ -1796,26 +1791,21 @@ export const IDL: MarinadeBeam = { }, { "code": 6001, - "name": "CalculationFailure", - "msg": "An error occurred during calculation" - }, - { - "code": 6002, "name": "InvalidEpochReportAccount", "msg": "The epoch report account has not been updated to the current epoch yet" }, { - "code": 6003, + "code": 6002, "name": "RemainingUnclaimableTicketAmount", "msg": "The total ordered ticket amount exceeds the amount in all found tickets" }, { - "code": 6004, + "code": 6003, "name": "DelayedUnstakeTicketsNotYetClaimable", "msg": "Delayed unstake tickets for the current epoch can not yet be claimed" }, { - "code": 6005, + "code": 6004, "name": "TooManyTicketsClaimed", "msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report" } diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index 173daa5..a64e08d 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -638,6 +638,11 @@ export type MarinadeLpBeam = { "code": 6001, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6002, + "name": "InsufficientYieldBalance", + "msg": "The yield balance is insufficient to extract yield" } ] }; @@ -1282,6 +1287,11 @@ export const IDL: MarinadeLpBeam = { "code": 6001, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6002, + "name": "InsufficientYieldBalance", + "msg": "The yield balance is insufficient to extract yield" } ] }; diff --git a/packages/sdks/marinade-lp/src/index.ts b/packages/sdks/marinade-lp/src/index.ts index d18484c..fc68580 100644 --- a/packages/sdks/marinade-lp/src/index.ts +++ b/packages/sdks/marinade-lp/src/index.ts @@ -3,7 +3,6 @@ import { Keypair, PublicKey, SystemProgram, - SYSVAR_CLOCK_PUBKEY, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, type TransactionInstruction, @@ -18,6 +17,7 @@ import { BeamInterface, deriveAuthorityAddress, MarinadeLpBeam, + requestIncreasedCUsIx, sendAndConfirmChecked, } from "@sunrisestake/beams-common"; import { StateAccount } from "./state.js"; @@ -264,7 +264,7 @@ export class MarinadeLpClient extends BeamInterface< }) .instruction(); - return new Transaction().add(instruction); + return new Transaction().add(requestIncreasedCUsIx(400_000), instruction); } /** @@ -350,15 +350,16 @@ export class MarinadeLpClient extends BeamInterface< state: this.stateAddress, marinadeState: this.state.proxyState, sunriseState: this.state.sunriseState, - msolMint: this.marinadeLp.marinade.mSolMint.address, - msolVault: this.state.msolTokenAccount, + yieldAccount: this.sunrise.state.yieldAccount, + liqPoolMint: this.marinadeLp.marinade.lpMint.address, + liqPoolTokenVault: this.marinadeLp.beamVault, vaultAuthority: this.vaultAuthority[0], + transferMsolTo: this.state.msolTokenAccount, liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, - treasuryMsolAccount: this.marinadeLp.marinade.treasuryMsolAccount, - yieldAccount: this.sunrise.state.yieldAccount, + liqPoolMsolLegAuthority: + await this.marinadeLp.marinade.mSolLegAuthority(), sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, - sysvarClock: SYSVAR_CLOCK_PUBKEY, sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -464,16 +465,6 @@ export class MarinadeLpClient extends BeamInterface< const solValue = solBalance * proportionOfPool; const msolValue = msolLegBalance.toNumber() * proportionOfPool; - console.log("calculateBalanceFromLpTokens", { - lpTokens: lpTokens.toNumber(), - totalSupply: totalSupply.toNumber(), - proportionOfPool, - solBalance, - msolLegBalance: msolLegBalance.toNumber(), - solValue, - msolValue, - }); - return { lamports: new BN(solValue), msolLamports: new BN(msolValue), diff --git a/packages/tests/src/functional/beams/marinade-lp.test.ts b/packages/tests/src/functional/beams/marinade-lp.test.ts index f886956..6637e1d 100644 --- a/packages/tests/src/functional/beams/marinade-lp.test.ts +++ b/packages/tests/src/functional/beams/marinade-lp.test.ts @@ -8,6 +8,7 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { + expectAmount, expectSolBalance, expectTokenBalance, fund, @@ -18,6 +19,7 @@ import { } from "../../utils.js"; import { expect } from "chai"; import { MarinadeClient } from "@sunrisestake/beams-marinade-sp"; +import { MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE, ZERO } from "../consts.js"; describe("Marinade liquidity pool beam", () => { let coreClient: SunriseClient; @@ -68,6 +70,11 @@ describe("Marinade liquidity pool beam", () => { provider.publicKey, sunriseStateAddress, ); + coreClient = await coreClient.refresh(); + await sendAndConfirmTransaction( + provider, + await coreClient.registerBeam(marinadeSpBeamClient.stateAddress), + ); await SunriseClient.get(provider, sunriseStateAddress); @@ -76,7 +83,7 @@ describe("Marinade liquidity pool beam", () => { provider.publicKey, sunriseStateAddress, marinadeSpBeamClient.stateAddress, - marinadeSpBeamClient.marinade.beamMsolVault, + marinadeSpBeamClient.marinade.beamMsolVault, // this will be the target of any extracted msol ); const info = beamClient.state.pretty(); @@ -193,9 +200,10 @@ describe("Marinade liquidity pool beam", () => { // check that the epoch report has been updated beamClient = await beamClient.refresh(); - expect( - beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), - ).to.equal(0); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount(beamEpochDetail.extractableYield, ZERO); }); it("can't deposit due to exceeding allocation", async () => { @@ -232,7 +240,8 @@ describe("Marinade liquidity pool beam", () => { await beamClient.proportionOfPool(withdrawalAmountBN); const lpSupply = await beamClient.poolTokenSupply(); const withdrawnLpTokens = new BN( - "" + Math.floor(lpSupply.toNumber() * proportionOfPool), + // round up to ensure we withdraw sufficient LP tokens to cover the required SOL amount + "" + Math.ceil(lpSupply.toNumber() * proportionOfPool), ); const expectedLPTokens = vaultBalance.sub(withdrawnLpTokens); @@ -256,9 +265,10 @@ describe("Marinade liquidity pool beam", () => { // check that the epoch report has been updated beamClient = await beamClient.refresh(); - expect( - beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), - ).to.equal(0); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount(beamEpochDetail.extractableYield, ZERO); }); it("can burn gsol", async () => { @@ -287,7 +297,8 @@ describe("Marinade liquidity pool beam", () => { // This results in less extractable yield for this beam, and more for the Marinade-SP beam. // (However, in reality, this beam should rarely be extracted from, as it is // included as a buffer to allow for fee-less gSOL withdrawals) - const expectedFee = burnAmount.toNumber() * 0.003; + const expectedFee = + burnAmount.toNumber() * MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE; const effectiveBurnedAmount = burnAmount.subn(expectedFee); const lpTokenValue = effectiveBurnedAmount.toNumber() / (await beamClient.poolTokenPrice()); @@ -295,15 +306,6 @@ describe("Marinade liquidity pool beam", () => { new BN(lpTokenValue), ); netExtractableYield = lpBalance.lamports; - console.log("burnAmount", burnAmount.toNumber()); - console.log("expectedFee", expectedFee); - console.log("effectiveBurnedAmount", effectiveBurnedAmount.toNumber()); - console.log("poolTokenPrice", await beamClient.poolTokenPrice()); - console.log("lpTokenValue", lpTokenValue); - console.log("lpBalance", { - lamports: lpBalance.lamports.toNumber(), - msolLamports: lpBalance.msolLamports.toNumber(), - }); await sendAndConfirmTransaction( // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test @@ -314,9 +316,14 @@ describe("Marinade liquidity pool beam", () => { // check that the epoch report has been updated beamClient = await beamClient.refresh(); - expect( - beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), - ).to.equal(netExtractableYield.toNumber()); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount( + netExtractableYield.toNumber(), + beamEpochDetail.extractableYield.toNumber(), + 2, + ); }); it("can extract yield into a stake account", async () => { diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 7a19bac..50516ca 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -25,8 +25,6 @@ import { } from "../../utils.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { expect } from "chai"; -import { MSOL_MINT } from "../consts.js"; - describe("Marinade stake pool beam", () => { let coreClient: SunriseClient; let beamClient: MarinadeClient; diff --git a/packages/tests/src/functional/consts.ts b/packages/tests/src/functional/consts.ts index b27ed91..45b245d 100644 --- a/packages/tests/src/functional/consts.ts +++ b/packages/tests/src/functional/consts.ts @@ -1,6 +1,11 @@ -// see anchor.toml import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +export const ZERO = new BN(0); +export const MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE = 0; + +// see anchor.toml export const SUNRISE_CORE_STATE = new PublicKey( "89wj5p56PTFiKQcHLTkx78jM3Cv4jVRCXgMKJvoFvvp", ); diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 5906651..1423202 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -101,8 +101,7 @@ pub mod marinade_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports); msg!("Liquid Unstake {} msol", msol_lamports); // CPI: Liquid unstake. @@ -130,8 +129,7 @@ pub mod marinade_beam { pub fn order_withdrawal(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports); // CPI: Order unstake and receive a Marinade unstake ticket. let accounts = ctx.accounts.deref().into(); @@ -211,8 +209,7 @@ pub mod marinade_beam { &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, )?; - let yield_msol = calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; + let yield_msol = calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports); let yield_account_balance_before = ctx.accounts.yield_account.lamports(); @@ -772,8 +769,6 @@ pub struct UpdateEpochReport<'info> { pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] NotDelegated, - #[msg("An error occurred during calculation")] - CalculationFailure, #[msg("The epoch report account has not been updated to the current epoch yet")] InvalidEpochReportAccount, #[msg("The total ordered ticket amount exceeds the amount in all found tickets")] diff --git a/programs/marinade-beam/src/system/utils.rs b/programs/marinade-beam/src/system/utils.rs index e7e4153..77d38af 100644 --- a/programs/marinade-beam/src/system/utils.rs +++ b/programs/marinade-beam/src/system/utils.rs @@ -15,8 +15,7 @@ pub fn calculate_extractable_yield( marinade_state: &MarinadeState, msol_vault: &TokenAccount, ) -> Result { - let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure))?; + let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount); let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index 1b1b04d..b36eb57 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -5,11 +5,8 @@ use sunrise_core as sunrise_core_cpi; use sunrise_core::cpi::accounts::{ExtractYield, UpdateEpochReport}; use sunrise_core_cpi::cpi::{ accounts::{BurnGsol, MintGsol, TransferGsol}, - burn_gsol as cpi_burn_gsol, - mint_gsol as cpi_mint_gsol, - transfer_gsol as cpi_transfer_gsol, - extract_yield as cpi_extract_yield, - update_epoch_report as cpi_update_epoch_report, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + transfer_gsol as cpi_transfer_gsol, update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index 27d66eb..b70111f 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -2,24 +2,23 @@ // Temporarily allow to pass clippy ci #![allow(dead_code)] +use crate::cpi_interface::program::Marinade; use anchor_lang::prelude::*; use anchor_spl::associated_token::{AssociatedToken, Create}; use anchor_spl::token::{Mint, Token, TokenAccount}; -use marinade_cpi::State as MarinadeState; -use std::ops::Deref; use cpi_interface::marinade_lp as marinade_lp_interface; use cpi_interface::sunrise as sunrise_interface; +use marinade_cpi::State as MarinadeState; use state::{State, StateEntry}; +use std::cmp::max; +use std::ops::Deref; +use sunrise_core as sunrise_core_cpi; use system::utils; mod cpi_interface; mod state; mod system; -// TODO: Use actual CPI crate. -use crate::cpi_interface::program::Marinade; -use sunrise_core as sunrise_core_cpi; - declare_id!("9Xek4q2hsdPm4yaRt4giQnVTTgRGwGhXQ1HBXbinuPTP"); mod constants { @@ -31,9 +30,10 @@ mod constants { #[program] pub mod marinade_lp_beam { - use marinade_common::calc_lamports_from_msol_amount; use super::*; use crate::cpi_interface::marinade_lp; + use crate::system::utils::get_extractable_yield_from_excess_balance; + use marinade_common::calc_lamports_from_msol_amount; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -75,13 +75,20 @@ pub mod marinade_lp_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate the number of liq_pool tokens that would be needed to withdraw `lamports` - let liq_pool_balance = utils::calculate_liq_pool_balance_required_to_withdraw_lamports( - &ctx.accounts.marinade_state, - &ctx.accounts.liq_pool_mint, - &ctx.accounts.liq_pool_sol_leg_pda, - &ctx.accounts.liq_pool_msol_leg, + let liq_pool_balance_to_withdraw = + utils::calculate_liq_pool_balance_required_to_withdraw_lamports( + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + lamports, + )?; + + msg!( + "Balance required to withdraw {} lamports: {:?}", lamports, - )?; + liq_pool_balance_to_withdraw + ); // CPI: Remove liquidity from Marinade liq_pool. The liq_pool tokens vault owned by // this vault is the source burning lp tokens. // The result is a combination of SOL and mSOL. @@ -95,7 +102,8 @@ pub mod marinade_lp_beam { &ctx.accounts.marinade_program, &ctx.accounts.state, accounts, - liq_pool_balance.liq_pool_token, + // this is safe, because the liq_pool_balance is guaranteed to be non-negative + liq_pool_balance_to_withdraw.liq_pool_token as u64, )?; let state_bump = ctx.bumps.state; @@ -110,8 +118,8 @@ pub mod marinade_lp_beam { let lamport_value_of_msol = calc_lamports_from_msol_amount( &ctx.accounts.marinade_state, - liq_pool_balance.msol - ).map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + liq_pool_balance_to_withdraw.msol as u64, + ); sunrise_interface::transfer_gsol( ctx.accounts.deref(), ctx.accounts.sunrise_program.to_account_info(), @@ -161,6 +169,15 @@ pub mod marinade_lp_beam { &ctx.accounts.liq_pool_msol_leg, )?; + // if there is insufficient balance to extract, return an error + // note - if this is negative, we have a shortfall in the pool + // this should only ever be max 1 or 2 lamports due to rounding + require_gt!( + yield_balance.liq_pool_token, + 0, + MarinadeLpBeamError::InsufficientYieldBalance + ); + let yield_account_balance_before = ctx.accounts.yield_account.lamports(); let accounts = ctx.accounts.deref().into(); @@ -168,7 +185,8 @@ pub mod marinade_lp_beam { &ctx.accounts.marinade_program, &ctx.accounts.state, accounts, - yield_balance.liq_pool_token, + // checked by the assert above - guaranteed to be positive + yield_balance.liq_pool_token as u64, )?; let yield_account_balance_after = ctx.accounts.yield_account.lamports(); @@ -201,18 +219,8 @@ pub mod marinade_lp_beam { &ctx.accounts.liq_pool_msol_leg, )?; - // in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens - // when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) - // the SOL is sent to the yield account, - // and the mSOL is sent to the beam's mSOL token account, which is typically - // the Marinade-SP's beam vault. - // This results in less extractable yield for this beam, and more for the Marinade-SP beam. - // (However, in reality, this beam should rarely be extracted from, as it is - // included as a buffer to allow for fee-less gSOL withdrawals) - // Subtract fee TODO can we do better than an estimate? - let extractable_lamports = (yield_balance.lamports as f64) * 0.997; // estimated 0.3% unstake fee - msg!("yield_balance: {:?}", yield_balance); - msg!("Extractable yield: {}", extractable_lamports); + let excess_lamports = max(0, yield_balance.lamports) as u64; + let extractable_lamports = get_extractable_yield_from_excess_balance(excess_lamports); // CPI: update the epoch report with the extracted yield. let state_bump = ctx.bumps.state; @@ -565,4 +573,6 @@ pub enum MarinadeLpBeamError { CalculationFailure, #[msg("This feature is unimplemented for this beam")] Unimplemented, + #[msg("The yield balance is insufficient to extract yield")] + InsufficientYieldBalance, } diff --git a/programs/marinade-lp-beam/src/system/balance.rs b/programs/marinade-lp-beam/src/system/balance.rs index 3dfe761..6209bcf 100644 --- a/programs/marinade-lp-beam/src/system/balance.rs +++ b/programs/marinade-lp-beam/src/system/balance.rs @@ -4,12 +4,12 @@ use marinade_cpi::State as MarinadeState; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct LiquidityPoolBalance { - pub lamports: u64, - pub msol: u64, - pub liq_pool_token: u64, + pub lamports: i128, + pub msol: i128, + pub liq_pool_token: i128, } impl LiquidityPoolBalance { - pub fn new(sol_leg: u64, msol_leg: u64, total_liq_pool_tokens: u64) -> Self { + pub fn new(sol_leg: i128, msol_leg: i128, total_liq_pool_tokens: i128) -> Self { LiquidityPoolBalance { lamports: sol_leg, msol: msol_leg, @@ -18,55 +18,48 @@ impl LiquidityPoolBalance { } pub fn value_of(&self, liq_pool_token: u64) -> Result { - let lamports = proportional(self.lamports, liq_pool_token, self.liq_pool_token) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - let msol = proportional(self.msol, liq_pool_token, self.liq_pool_token) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let lamports = proportional(self.lamports, liq_pool_token as i128, self.liq_pool_token); + let msol = proportional(self.msol, liq_pool_token as i128, self.liq_pool_token); Ok(LiquidityPoolBalance { lamports, msol, - liq_pool_token, + liq_pool_token: liq_pool_token as i128, }) } // The value of both legs of the liquidity pool balance in SOL pub fn sol_value(&self, marinade_state: &MarinadeState) -> u64 { let lamports = self.lamports; - let msol = calc_lamports_from_msol_amount(marinade_state, self.msol) - .unwrap(); - lamports.checked_add(msol).expect("sol_value") + let msol = calc_lamports_from_msol_amount(marinade_state, self.msol as u64); + lamports.checked_add(msol as i128).expect("sol_value") as u64 } // if this balance in lamports is smaller than other_lamports, return this, // otherwise return a liquidity pool balance with lamports = other_lamports // and liq_pool_token = the amount of liq_pool_token that would be needed to withdraw // other_lamports from the liquidity pool - pub fn min_lamports(&self, other_lamports: u64) -> Result { + pub fn min_lamports(&self, other_lamports: i128) -> Self { if self.lamports < other_lamports { - return Ok(*self); + return *self; } - let other_liq_pool_token = proportional(self.liq_pool_token, other_lamports, self.lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - let other_msol = proportional(self.msol, other_lamports, self.lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - Ok(Self { + let other_liq_pool_token = proportional(self.liq_pool_token, other_lamports, self.lamports); + let other_msol = proportional(self.msol, other_lamports, self.lamports); + Self { lamports: other_lamports, msol: other_msol, liq_pool_token: other_liq_pool_token, - }) + } } // returns a new balance that is the result of subtracting other_lamports from this balance - pub fn checked_sub_lamports(&self, other_lamports: u64) -> Result { + pub fn checked_sub_lamports(&self, other_lamports: i128) -> Result { let new_lamports = self .lamports .checked_sub(other_lamports) .expect("checked_sub_lamports"); - let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports); - let new_msol = proportional(self.msol, new_lamports, self.lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let new_msol = proportional(self.msol, new_lamports, self.lamports); Ok(Self { lamports: new_lamports, msol: new_msol, @@ -74,23 +67,14 @@ impl LiquidityPoolBalance { }) } - pub fn checked_sub(&self, other: Self) -> Result { - let new_lamports = self - .lamports - .checked_sub(other.lamports) - .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - let new_liq_pool_token = self - .liq_pool_token - .checked_sub(other.liq_pool_token) - .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - let new_msol = self - .msol - .checked_sub(other.msol) - .ok_or_else(|| error!(crate::MarinadeLpBeamError::CalculationFailure))?; - Ok(Self { + pub fn sub(&self, other: Self) -> Self { + let new_lamports = self.lamports.saturating_sub(other.lamports); + let new_liq_pool_token = self.liq_pool_token.saturating_sub(other.liq_pool_token); + let new_msol = self.msol.saturating_sub(other.msol); + Self { lamports: new_lamports, msol: new_msol, liq_pool_token: new_liq_pool_token, - }) + } } } diff --git a/programs/marinade-lp-beam/src/system/utils.rs b/programs/marinade-lp-beam/src/system/utils.rs index e544486..c6fe140 100644 --- a/programs/marinade-lp-beam/src/system/utils.rs +++ b/programs/marinade-lp-beam/src/system/utils.rs @@ -2,10 +2,16 @@ use super::balance::LiquidityPoolBalance; use crate::state::State; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; -use marinade_common::{calc_lamports_from_msol_amount, proportional}; +use marinade_common::{ + calc_lamports_from_msol_amount, proportional, proportional_with_rounding, RoundingMode, +}; use marinade_cpi::State as MarinadeState; use sunrise_core::BeamError; +// estimated 0.3% unstake fee +// const WITHDRAWAL_FRACTION : f64 = 0.997; +const WITHDRAWAL_FRACTION: f64 = 1.0; + /// Calculates the amount that can be extracted as yield, in lamports. pub fn calculate_extractable_yield( sunrise_state: &sunrise_core::State, @@ -40,11 +46,9 @@ pub fn calculate_extractable_yield( liq_pool_sol_leg_pda, liq_pool_msol_leg, staked_sol, - )?; + ); msg!("staked_balance: {:?}", staked_balance); - msg!("staked_sol: {:?}", staked_sol); - msg!("required_lp_tokens_to_cover_staked_sol: {:?}", required_lp_tokens_to_cover_staked_sol); let required_liq_pool_balance = liq_pool_balance_for_tokens( required_lp_tokens_to_cover_staked_sol, @@ -56,16 +60,32 @@ pub fn calculate_extractable_yield( msg!("required_liq_pool_balance: {:?}", required_liq_pool_balance); // return the difference between the staked balance and the required balance - let diff = staked_balance.checked_sub(required_liq_pool_balance)?; + let diff = staked_balance.sub(required_liq_pool_balance); msg!("diff: {:?}", diff); Ok(diff) } +// in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens +// when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) +// the SOL is sent to the yield account, +// and the mSOL is sent to the beam's mSOL token account, which is typically +// the Marinade-SP's beam vault. +// This results in less extractable yield for this beam, and more for the Marinade-SP beam. +// (However, in reality, this beam should rarely be extracted from, as it is +// included as a buffer to allow for fee-less gSOL withdrawals) +// Subtract fee TODO can we do better than an estimate? +pub fn get_extractable_yield_from_excess_balance(excess_balance: u64) -> u64 { + let extractable_lamports = (excess_balance as f64) * WITHDRAWAL_FRACTION; + msg!("Excess balance: {:?}", excess_balance); + msg!("Extractable yield: {}", extractable_lamports); + extractable_lamports as u64 +} + // Prevent the compiler from enlarging the stack and potentially triggering an Access violation #[inline(never)] /// Returns the current liquidity pool balance owned by the beam -pub(super) fn current_liq_pool_balance( +pub fn current_liq_pool_balance( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_token_account: &TokenAccount, @@ -123,18 +143,16 @@ pub fn calculate_liq_pool_token_value_of_lamports( liq_pool_sol_leg_pda: &AccountInfo, liq_pool_msol_leg: &TokenAccount, lamports: u64, -) -> Result { +) -> u64 { let total_lamports = liq_pool_sol_leg_pda .lamports() .checked_sub(marinade_state.rent_exempt_for_token_acc) .unwrap(); let total_msol = liq_pool_msol_leg.amount; - let lamports_value_of_msol = calc_lamports_from_msol_amount(marinade_state, total_msol) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + let lamports_value_of_msol = calc_lamports_from_msol_amount(marinade_state, total_msol); let total_value_of_pool = total_lamports.checked_add(lamports_value_of_msol).unwrap(); proportional(liq_pool_mint.supply, lamports, total_value_of_pool) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) } fn total_liq_pool( @@ -149,9 +167,9 @@ fn total_liq_pool( .expect("sol_leg_lamports"); LiquidityPoolBalance::new( - sol_leg_lamports, - liq_pool_msol_leg.amount, - liq_pool_mint.supply, + sol_leg_lamports as i128, + liq_pool_msol_leg.amount as i128, + liq_pool_mint.supply as i128, ) } @@ -168,14 +186,19 @@ pub fn calculate_liq_pool_balance_required_to_withdraw_lamports( .unwrap(); let liq_pool_mint_supply = liq_pool_mint.supply; - let liq_pool_tokens = proportional(liq_pool_mint_supply, lamports, liq_pool_lamports) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure))?; + // Round up to ensure that the amount of LP tokens is enough to withdraw the required amount of SOL + let liq_pool_tokens = proportional_with_rounding( + liq_pool_mint_supply, + lamports, + liq_pool_lamports, + RoundingMode::Up, + ); liq_pool_balance_for_tokens( liq_pool_tokens, marinade_state, liq_pool_mint, liq_pool_sol_leg_pda, - liq_pool_msol_leg + liq_pool_msol_leg, ) } diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index fa058c1..0ff4e03 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -23,7 +23,10 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { return Err(BeamError::BurnWindowExceeded.into()); } - details.partial_gsol_supply = details.partial_gsol_supply.checked_sub(amount_in_lamports).unwrap(); + details.partial_gsol_supply = details + .partial_gsol_supply + .checked_sub(amount_in_lamports) + .unwrap(); token::burn( amount_in_lamports, &ctx.accounts.gsol_mint.to_account_info(), diff --git a/programs/sunrise-core/src/instructions/mod.rs b/programs/sunrise-core/src/instructions/mod.rs index f13acd1..1771990 100644 --- a/programs/sunrise-core/src/instructions/mod.rs +++ b/programs/sunrise-core/src/instructions/mod.rs @@ -6,10 +6,10 @@ pub mod register_beam; pub mod register_state; pub mod remove_beam; pub mod resize_allocations; +pub mod transfer_gsol; pub mod update_allocations; pub mod update_epoch_report; pub mod update_state; -pub mod transfer_gsol; pub use burn_gsol::*; pub use export_mint_authority::*; @@ -19,7 +19,7 @@ pub use register_beam::*; pub use register_state::*; pub use remove_beam::*; pub use resize_allocations::*; +pub use transfer_gsol::*; pub use update_allocations::*; pub use update_epoch_report::*; pub use update_state::*; -pub use transfer_gsol::*; diff --git a/programs/sunrise-core/src/instructions/transfer_gsol.rs b/programs/sunrise-core/src/instructions/transfer_gsol.rs index 1f34acd..1daea16 100644 --- a/programs/sunrise-core/src/instructions/transfer_gsol.rs +++ b/programs/sunrise-core/src/instructions/transfer_gsol.rs @@ -1,7 +1,11 @@ use crate::{system, utils, BeamError, TransferGsol}; use anchor_lang::prelude::*; -pub fn handler(ctx: Context, recipient_beam: Pubkey, amount_in_lamports: u64) -> Result<()> { +pub fn handler( + ctx: Context, + recipient_beam: Pubkey, + amount_in_lamports: u64, +) -> Result<()> { let amount = amount_in_lamports; let state = &mut ctx.accounts.state; @@ -23,12 +27,18 @@ pub fn handler(ctx: Context, recipient_beam: Pubkey, amount_in_lam ); return Err(BeamError::BurnWindowExceeded.into()); } - source_beam_details.partial_gsol_supply = source_beam_details.partial_gsol_supply.checked_sub(amount).unwrap(); + source_beam_details.partial_gsol_supply = source_beam_details + .partial_gsol_supply + .checked_sub(amount) + .unwrap(); let target_beam_details = state .get_mut_beam_details(&recipient_beam) .ok_or(BeamError::UnidentifiedBeam)?; - target_beam_details.partial_gsol_supply = target_beam_details.partial_gsol_supply.checked_add(amount).unwrap(); + target_beam_details.partial_gsol_supply = target_beam_details + .partial_gsol_supply + .checked_add(amount) + .unwrap(); Ok(()) } diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index 12b79ae..0bb6296 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -78,7 +78,11 @@ pub mod sunrise_core { /// /// Same invariants as for [minting][sunrise_core::mint_gsol()]. /// Errors if the recipient beam is not registered in the state. - pub fn transfer_gsol(ctx: Context, recipient_beam: Pubkey, amount: u64) -> Result<()> { + pub fn transfer_gsol( + ctx: Context, + recipient_beam: Pubkey, + amount: u64, + ) -> Result<()> { transfer_gsol::handler(ctx, recipient_beam, amount) } diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index d4b7704..5d8aad9 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -308,6 +308,10 @@ impl EpochReport { .ok_or(BeamError::Overflow) .unwrap(); + // The extractable yield should be reduced (most likely to zero) + beam_details.extractable_yield = + beam_details.extractable_yield.saturating_sub(yield_amount); + Ok(()) } From 24ea5666db8bf2c5abec3d75032a6b5b5159737e Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 29 Feb 2024 11:02:11 +0100 Subject: [PATCH 7/8] Marinade LP: increase reliability of functional tests --- Anchor.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Anchor.toml b/Anchor.toml index 42a4a8e..0c3f49a 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -20,6 +20,9 @@ wallet = "packages/tests/fixtures/provider.json" [scripts] test = "packages/tests/run.sh" +[test] +startup_wait = 3000 + [test.validator] slots_per_epoch = "32" #url = "https://api.mainnet-beta.solana.com" From 7a74040a6677426e6d7ff16c050ab1a60b115bfd Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 29 Feb 2024 11:22:26 +0100 Subject: [PATCH 8/8] Marinade LP - test fixes - stub the clock sysvar --- programs/sunrise-core/src/state.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 5d8aad9..2843c3f 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -351,13 +351,21 @@ impl BeamEpochDetails { mod internal_tests { use super::*; + // Stub the Clock sysvar + struct SyscallStubs {} + impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { + fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { + 0 + } + } + #[test] fn test_register_beam() { let mut state = State::default(); let mut input = RegisterStateInput::default(); input.initial_capacity = 10; - state.register(input, 0, &Pubkey::default(), 1000); + state.register(input, 0, &Pubkey::default(), 1000).unwrap(); assert_eq!(state.allocations, vec![BeamDetails::default(); 10]); } #[test] @@ -390,13 +398,17 @@ mod internal_tests { assert_eq!(state.beam_count(), 2); } + #[test] fn test_add_beam() { + solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); let mut state = State::default(); let mut input = RegisterStateInput::default(); input.initial_capacity = 2; - state.register(input, 0, &Pubkey::new_unique(), 1000); + state + .register(input, 0, &Pubkey::new_unique(), 1000) + .unwrap(); let beam_key = Pubkey::new_unique(); let new_beam = BeamDetails::new(beam_key, 10); @@ -505,12 +517,10 @@ mod internal_tests { let initial_size = state.size_inner(); assert_eq!(state.allocations.len(), 0); - assert_eq!(initial_size, State::SIZE_WITH_ZERO_BEAMS); assert_eq!(initial_size, State::size(0)); state.allocations = vec![BeamDetails::default(); 10]; let size = state.size_inner(); assert_eq!(size, State::size(10)); - assert_eq!(size, initial_size + (10 * BeamDetails::SIZE)) } }